| File: | Volumes/Data/worker/Apple-iOS-26-Safer-CPP-Checks-EWS/build/Source/WebCore/editing/Editor.cpp |
| Warning: | line 4815, column 15 Local variable 'cachedImage' is uncounted and unsafe |
Press '?' to see keyboard shortcuts
Keyboard shortcuts:
| 1 | /* |
| 2 | * Copyright (C) 2006-2025 Apple Inc. All rights reserved. |
| 3 | * Copyright (C) 2008 Nokia Corporation and/or its subsidiary(-ies) |
| 4 | * Copyright (C) 2025 Samuel Weinig <sam@webkit.org> |
| 5 | * |
| 6 | * Redistribution and use in source and binary forms, with or without |
| 7 | * modification, are permitted provided that the following conditions |
| 8 | * are met: |
| 9 | * 1. Redistributions of source code must retain the above copyright |
| 10 | * notice, this list of conditions and the following disclaimer. |
| 11 | * 2. Redistributions in binary form must reproduce the above copyright |
| 12 | * notice, this list of conditions and the following disclaimer in the |
| 13 | * documentation and/or other materials provided with the distribution. |
| 14 | * |
| 15 | * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY |
| 16 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| 17 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| 18 | * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR |
| 19 | * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
| 20 | * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
| 21 | * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
| 22 | * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY |
| 23 | * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| 25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| 26 | */ |
| 27 | |
| 28 | #include "config.h" |
| 29 | #include "Editor.h" |
| 30 | |
| 31 | #include "AXObjectCache.h" |
| 32 | #include "AlternativeTextController.h" |
| 33 | #include "ApplyStyleCommand.h" |
| 34 | #include "AttachmentAssociatedElement.h" |
| 35 | #include "BoundaryPointInlines.h" |
| 36 | #include "CSSComputedStyleDeclaration.h" |
| 37 | #include "CSSPropertyNames.h" |
| 38 | #include "CSSSerializationContext.h" |
| 39 | #include "CSSValueList.h" |
| 40 | #include "CSSValuePool.h" |
| 41 | #include "CaretRectComputation.h" |
| 42 | #include "ChangeListTypeCommand.h" |
| 43 | #include "Chrome.h" |
| 44 | #include "ChromeClient.h" |
| 45 | #include "ClipboardEvent.h" |
| 46 | #include "CommonAtomStrings.h" |
| 47 | #include "CompositionEvent.h" |
| 48 | #include "CompositionHighlight.h" |
| 49 | #include "ContainerNodeInlines.h" |
| 50 | #include "CreateLinkCommand.h" |
| 51 | #include "CustomUndoStep.h" |
| 52 | #include "DataTransfer.h" |
| 53 | #include "DeleteSelectionCommand.h" |
| 54 | #include "DeprecatedGlobalSettings.h" |
| 55 | #include "DictationAlternative.h" |
| 56 | #include "DictationCommand.h" |
| 57 | #include "DocumentFragment.h" |
| 58 | #include "DocumentInlines.h" |
| 59 | #include "DocumentMarkerController.h" |
| 60 | #include "DocumentMarkers.h" |
| 61 | #include "DocumentPage.h" |
| 62 | #include "DocumentQuirks.h" |
| 63 | #include "DocumentResourceLoader.h" |
| 64 | #include "DocumentView.h" |
| 65 | #include "Editing.h" |
| 66 | #include "EditorClient.h" |
| 67 | #include "ElementAncestorIteratorInlines.h" |
| 68 | #include "EventHandler.h" |
| 69 | #include "EventNames.h" |
| 70 | #include "File.h" |
| 71 | #include "FocusController.h" |
| 72 | #include "FontAttributes.h" |
| 73 | #include "FrameDestructionObserverInlines.h" |
| 74 | #include "FrameInlines.h" |
| 75 | #include "FrameLoader.h" |
| 76 | #include "FrameTree.h" |
| 77 | #include "GraphicsContext.h" |
| 78 | #include "HTMLAttachmentElement.h" |
| 79 | #include "HTMLBRElement.h" |
| 80 | #include "HTMLBodyElement.h" |
| 81 | #include "HTMLFormControlElement.h" |
| 82 | #include "HTMLFrameOwnerElement.h" |
| 83 | #include "HTMLImageElement.h" |
| 84 | #include "HTMLInputElement.h" |
| 85 | #include "HTMLNames.h" |
| 86 | #include "HTMLOListElement.h" |
| 87 | #include "HTMLQuoteElement.h" |
| 88 | #include "HTMLSpanElement.h" |
| 89 | #include "HTMLTextAreaElement.h" |
| 90 | #include "HTMLUListElement.h" |
| 91 | #include "HitTestResult.h" |
| 92 | #include "ImageOverlay.h" |
| 93 | #include "IndentOutdentCommand.h" |
| 94 | #include "InputEvent.h" |
| 95 | #include "InsertListCommand.h" |
| 96 | #include "InsertTextCommand.h" |
| 97 | #include "KeyboardEvent.h" |
| 98 | #include "LocalFrameInlines.h" |
| 99 | #include "LocalFrameView.h" |
| 100 | #include "Logging.h" |
| 101 | #include "ModifySelectionListLevel.h" |
| 102 | #include "NodeList.h" |
| 103 | #include "NodeTraversal.h" |
| 104 | #include "PagePasteboardContext.h" |
| 105 | #include "Pasteboard.h" |
| 106 | #include "Range.h" |
| 107 | #include "RemoveFormatCommand.h" |
| 108 | #include "RenderAncestorIterator.h" |
| 109 | #include "RenderBlock.h" |
| 110 | #include "RenderBlockFlow.h" |
| 111 | #include "RenderImage.h" |
| 112 | #include "RenderInline.h" |
| 113 | #include "RenderLayer.h" |
| 114 | #include "RenderObjectStyle.h" |
| 115 | #include "RenderStyle+GettersInlines.h" |
| 116 | #include "RenderTextControl.h" |
| 117 | #include "RenderedDocumentMarker.h" |
| 118 | #include "RenderedPosition.h" |
| 119 | #include "ReplaceRangeWithTextCommand.h" |
| 120 | #include "ReplaceSelectionCommand.h" |
| 121 | #include "ScriptDisallowedScope.h" |
| 122 | #include "SerializedAttachmentData.h" |
| 123 | #include "Settings.h" |
| 124 | #include "ShadowRoot.h" |
| 125 | #include "SharedBuffer.h" |
| 126 | #include "SimplifyMarkupCommand.h" |
| 127 | #include "SpellChecker.h" |
| 128 | #include "SpellingCorrectionCommand.h" |
| 129 | #include "StaticPasteboard.h" |
| 130 | #include "StylePropertiesInlines.h" |
| 131 | #include "StyleTextShadow.h" |
| 132 | #include "StyleTreeResolver.h" |
| 133 | #include "StyleVerticalAlign.h" |
| 134 | #include "SystemSoundManager.h" |
| 135 | #include "TelephoneNumberDetector.h" |
| 136 | #include "Text.h" |
| 137 | #include "TextCheckerClient.h" |
| 138 | #include "TextCheckingHelper.h" |
| 139 | #include "TextEvent.h" |
| 140 | #include "TextIterator.h" |
| 141 | #include "TextNodeTraversal.h" |
| 142 | #include "TextPlaceholderElement.h" |
| 143 | #include "TypingCommand.h" |
| 144 | #include "UserTypingGestureIndicator.h" |
| 145 | #include "VisibleUnits.h" |
| 146 | #include "WritingSuggestionData.h" |
| 147 | #include "markup.h" |
| 148 | #include <pal/FileSizeFormatter.h> |
| 149 | #include <pal/text/KillRing.h> |
| 150 | #include <wtf/Scope.h> |
| 151 | #include <wtf/SetForScope.h> |
| 152 | #include <wtf/TZoneMallocInlines.h> |
| 153 | #include <wtf/text/CharacterProperties.h> |
| 154 | #include <wtf/text/MakeString.h> |
| 155 | #include <wtf/text/ParsingUtilities.h> |
| 156 | #include <wtf/unicode/CharacterNames.h> |
| 157 | |
| 158 | #if PLATFORM(MAC)(defined WTF_PLATFORM_MAC && WTF_PLATFORM_MAC) |
| 159 | #include "ServicesOverlayController.h" |
| 160 | #endif |
| 161 | |
| 162 | #if ENABLE(ATTACHMENT_ELEMENT)(defined 1 && 1) |
| 163 | #include "PromisedAttachmentInfo.h" |
| 164 | #endif |
| 165 | |
| 166 | namespace WebCore { |
| 167 | |
| 168 | WTF_MAKE_TZONE_ALLOCATED_IMPL(TemporarySelectionChange)::bmalloc::api::HeapRef TemporarySelectionChange::s_heapRef; const TZoneSpecification TemporarySelectionChange::s_heapSpec = { & TemporarySelectionChange::s_heapRef, TZoneSpecification::encodeSize <TemporarySelectionChange>(), TZoneSpecification::encodeAlignment <TemporarySelectionChange>(), TZoneSpecification::encodeCategory <TemporarySelectionChange>(), ::bmalloc::api::compactAllocationMode <TemporarySelectionChange>(), TZoneSpecification::encodeDescriptor <TemporarySelectionChange>(), }; void* TemporarySelectionChange ::operatorNewSlow(size_t size) { if constexpr (::bmalloc::api ::compactAllocationMode<TemporarySelectionChange>() == CompactAllocationMode ::Compact) return ::bmalloc::api::tzoneAllocateCompactSlow(size , s_heapSpec); return ::bmalloc::api::tzoneAllocateNonCompactSlow (size, s_heapSpec); } using __makeBtzoneMallocedInlineMacroSemicolonifier __attribute__((unused)) = int; |
| 169 | WTF_MAKE_TZONE_ALLOCATED_IMPL(IgnoreSelectionChangeForScope)::bmalloc::api::HeapRef IgnoreSelectionChangeForScope::s_heapRef ; const TZoneSpecification IgnoreSelectionChangeForScope::s_heapSpec = { &IgnoreSelectionChangeForScope::s_heapRef, TZoneSpecification ::encodeSize<IgnoreSelectionChangeForScope>(), TZoneSpecification ::encodeAlignment<IgnoreSelectionChangeForScope>(), TZoneSpecification ::encodeCategory<IgnoreSelectionChangeForScope>(), ::bmalloc ::api::compactAllocationMode<IgnoreSelectionChangeForScope >(), TZoneSpecification::encodeDescriptor<IgnoreSelectionChangeForScope >(), }; void* IgnoreSelectionChangeForScope::operatorNewSlow (size_t size) { if constexpr (::bmalloc::api::compactAllocationMode <IgnoreSelectionChangeForScope>() == CompactAllocationMode ::Compact) return ::bmalloc::api::tzoneAllocateCompactSlow(size , s_heapSpec); return ::bmalloc::api::tzoneAllocateNonCompactSlow (size, s_heapSpec); } using __makeBtzoneMallocedInlineMacroSemicolonifier __attribute__((unused)) = int; |
| 170 | WTF_MAKE_TZONE_ALLOCATED_IMPL(Editor)::bmalloc::api::HeapRef Editor::s_heapRef; const TZoneSpecification Editor::s_heapSpec = { &Editor::s_heapRef, TZoneSpecification ::encodeSize<Editor>(), TZoneSpecification::encodeAlignment <Editor>(), TZoneSpecification::encodeCategory<Editor >(), ::bmalloc::api::compactAllocationMode<Editor>() , TZoneSpecification::encodeDescriptor<Editor>(), }; void * Editor::operatorNewSlow(size_t size) { if constexpr (::bmalloc ::api::compactAllocationMode<Editor>() == CompactAllocationMode ::Compact) return ::bmalloc::api::tzoneAllocateCompactSlow(size , s_heapSpec); return ::bmalloc::api::tzoneAllocateNonCompactSlow (size, s_heapSpec); } using __makeBtzoneMallocedInlineMacroSemicolonifier __attribute__((unused)) = int; |
| 171 | |
| 172 | static bool dispatchBeforeInputEvent(Element& element, const AtomString& inputType, IsInputMethodComposing isInputMethodComposing, const String& data = { }, |
| 173 | RefPtr<DataTransfer>&& dataTransfer = nullptr, const Vector<Ref<StaticRange>>& targetRanges = { }, Event::IsCancelable cancelable = Event::IsCancelable::Yes) |
| 174 | { |
| 175 | auto event = InputEvent::create(eventNames().beforeinputEvent, inputType, cancelable, element.document().windowProxy(), data, |
| 176 | WTF::move(dataTransfer), targetRanges, 0, isInputMethodComposing); |
| 177 | element.dispatchEvent(event); |
| 178 | return !event->defaultPrevented(); |
| 179 | } |
| 180 | |
| 181 | static void dispatchInputEvent(Element& element, const AtomString& inputType, IsInputMethodComposing isInputMethodComposing, const String& data = { }, |
| 182 | RefPtr<DataTransfer>&& dataTransfer = nullptr, const Vector<Ref<StaticRange>>& targetRanges = { }) |
| 183 | { |
| 184 | // FIXME: We should not be dispatching to the scoped queue here. Normally, input events are dispatched in CompositeEditCommand::apply after the end of the scope, |
| 185 | // but TypingCommands are special in that existing TypingCommands that are applied again fire input events *from within* the scope by calling typingAddedToOpenCommand. |
| 186 | // Instead, TypingCommands should always dispatch events synchronously after the end of the scoped queue in CompositeEditCommand::apply. To work around this for the |
| 187 | // time being, just revert back to calling dispatchScopedEvent. |
| 188 | element.dispatchScopedEvent(InputEvent::create(eventNames().inputEvent, inputType, Event::IsCancelable::No, element.document().windowProxy(), data, |
| 189 | WTF::move(dataTransfer), targetRanges, 0, isInputMethodComposing)); |
| 190 | } |
| 191 | |
| 192 | static String inputEventDataForEditingStyleAndAction(const StyleProperties* style, EditAction action) |
| 193 | { |
| 194 | if (!style) |
| 195 | return { }; |
| 196 | |
| 197 | switch (action) { |
| 198 | case EditAction::SetColor: |
| 199 | return style->getPropertyValue(CSSPropertyColor); |
| 200 | case EditAction::SetInlineWritingDirection: |
| 201 | case EditAction::SetBlockWritingDirection: |
| 202 | return style->getPropertyValue(CSSPropertyDirection); |
| 203 | default: |
| 204 | return { }; |
| 205 | } |
| 206 | } |
| 207 | |
| 208 | static String inputEventDataForEditingStyleAndAction(EditingStyle& style, EditAction action) |
| 209 | { |
| 210 | return inputEventDataForEditingStyleAndAction(style.style(), action); |
| 211 | } |
| 212 | |
| 213 | class ClearTextCommand : public DeleteSelectionCommand { |
| 214 | public: |
| 215 | ClearTextCommand(Ref<Document>&&); |
| 216 | static void CreateAndApply(Ref<Document>&&); |
| 217 | |
| 218 | private: |
| 219 | EditAction editingAction() const override; |
| 220 | }; |
| 221 | |
| 222 | ClearTextCommand::ClearTextCommand(Ref<Document>&& document) |
| 223 | : DeleteSelectionCommand(WTF::move(document), false, true, false, false, true) |
| 224 | { |
| 225 | } |
| 226 | |
| 227 | EditAction ClearTextCommand::editingAction() const |
| 228 | { |
| 229 | return EditAction::Delete; |
| 230 | } |
| 231 | |
| 232 | void ClearTextCommand::CreateAndApply(Ref<Document>&& document) |
| 233 | { |
| 234 | if (document->selection().isNone()) |
| 235 | return; |
| 236 | |
| 237 | // Don't leave around stale composition state. |
| 238 | document->editor().clear(); |
| 239 | |
| 240 | const VisibleSelection oldSelection = document->selection().selection(); |
| 241 | document->selection().selectAll(); |
| 242 | auto clearCommand = adoptRef(*new ClearTextCommand(WTF::move(document))); |
| 243 | clearCommand->setStartingSelection(oldSelection); |
| 244 | clearCommand->apply(); |
| 245 | } |
| 246 | |
| 247 | using namespace HTMLNames; |
| 248 | using namespace WTF::Unicode; |
| 249 | |
| 250 | TemporarySelectionChange::TemporarySelectionChange(Document& document, std::optional<VisibleSelection> temporarySelection, OptionSet<TemporarySelectionOption> options) |
| 251 | : m_document(&document) |
| 252 | , m_options(options) |
| 253 | , m_wasIgnoringSelectionChanges(document.editor().ignoreSelectionChanges()) |
| 254 | #if PLATFORM(IOS_FAMILY)(defined 1 && 1) |
| 255 | , m_appearanceUpdatesWereEnabled(document.selection().isUpdateAppearanceEnabled()) |
| 256 | #endif |
| 257 | { |
| 258 | #if PLATFORM(IOS_FAMILY)(defined 1 && 1) |
| 259 | if (options & TemporarySelectionOption::EnableAppearanceUpdates) |
| 260 | document.selection().setUpdateAppearanceEnabled(true); |
| 261 | #endif |
| 262 | |
| 263 | if (options & TemporarySelectionOption::IgnoreSelectionChanges) |
| 264 | document.editor().setIgnoreSelectionChanges(true); |
| 265 | |
| 266 | if (temporarySelection) { |
| 267 | m_selectionToRestore = document.selection().selection(); |
| 268 | #if PLATFORM(IOS_FAMILY)(defined 1 && 1) |
| 269 | if (document.selection().isUpdateAppearanceEnabled()) |
| 270 | document.selection().setNeedsSelectionUpdate(); |
| 271 | #endif |
| 272 | setSelection(temporarySelection.value(), IsTemporarySelection::Yes); |
| 273 | } |
| 274 | } |
| 275 | |
| 276 | void TemporarySelectionChange::invalidate() |
| 277 | { |
| 278 | if (RefPtr document = std::exchange(m_document, nullptr)) |
| 279 | document->editor().setIgnoreSelectionChanges(false, Editor::RevealSelection::No); |
| 280 | } |
| 281 | |
| 282 | TemporarySelectionChange::~TemporarySelectionChange() |
| 283 | { |
| 284 | if (!m_document) |
| 285 | return; |
| 286 | |
| 287 | if (m_selectionToRestore) |
| 288 | setSelection(m_selectionToRestore.value(), IsTemporarySelection::No); |
| 289 | |
| 290 | if (m_options & TemporarySelectionOption::IgnoreSelectionChanges) { |
| 291 | auto revealSelection = m_options & TemporarySelectionOption::RevealSelection ? Editor::RevealSelection::Yes : Editor::RevealSelection::No; |
| 292 | m_document->editor().setIgnoreSelectionChanges(m_wasIgnoringSelectionChanges, revealSelection); |
| 293 | } |
| 294 | |
| 295 | #if PLATFORM(IOS_FAMILY)(defined 1 && 1) |
| 296 | if (m_options & TemporarySelectionOption::EnableAppearanceUpdates) |
| 297 | m_document->selection().setUpdateAppearanceEnabled(m_appearanceUpdatesWereEnabled); |
| 298 | #endif |
| 299 | } |
| 300 | |
| 301 | void TemporarySelectionChange::setSelection(const VisibleSelection& selection, IsTemporarySelection isTemporarySelection) |
| 302 | { |
| 303 | auto options = FrameSelection::defaultSetSelectionOptions(); |
| 304 | |
| 305 | if (m_options & TemporarySelectionOption::UserTriggered) |
| 306 | options.add(FrameSelection::SetSelectionOption::IsUserTriggered); |
| 307 | |
| 308 | if (m_options & TemporarySelectionOption::DoNotSetFocus) |
| 309 | options.add(FrameSelection::SetSelectionOption::DoNotSetFocus); |
| 310 | |
| 311 | if (isTemporarySelection == IsTemporarySelection::Yes) { |
| 312 | if (m_options & TemporarySelectionOption::RevealSelection) |
| 313 | options.add(FrameSelection::SetSelectionOption::RevealSelection); |
| 314 | if (m_options & TemporarySelectionOption::DelegateMainFrameScroll) |
| 315 | options.add(FrameSelection::SetSelectionOption::DelegateMainFrameScroll); |
| 316 | if (m_options & TemporarySelectionOption::SmoothScroll) |
| 317 | options.add(FrameSelection::SetSelectionOption::SmoothScroll); |
| 318 | if (m_options & TemporarySelectionOption::RevealSelectionBounds) |
| 319 | options.add(FrameSelection::SetSelectionOption::RevealSelectionBounds); |
| 320 | if (m_options & TemporarySelectionOption::ForceCenterScroll) |
| 321 | options.add(FrameSelection::SetSelectionOption::ForceCenterScroll); |
| 322 | if (m_options & TemporarySelectionOption::OnlyAllowForwardScrolling) |
| 323 | options.add(FrameSelection::SetSelectionOption::OnlyAllowForwardScrolling); |
| 324 | } |
| 325 | |
| 326 | m_document->selection().setSelection(selection, options); |
| 327 | } |
| 328 | |
| 329 | IgnoreSelectionChangeForScope::IgnoreSelectionChangeForScope(LocalFrame& frame) |
| 330 | : m_selectionChange(*frame.document(), std::nullopt, TemporarySelectionOption::IgnoreSelectionChanges) |
| 331 | { |
| 332 | } |
| 333 | |
| 334 | IgnoreSelectionChangeForScope::~IgnoreSelectionChangeForScope() = default; |
| 335 | |
| 336 | // When an event handler has moved the selection outside of a text control |
| 337 | // we should use the target control's selection for this editing operation. |
| 338 | VisibleSelection Editor::selectionForCommand(Event* event) |
| 339 | { |
| 340 | auto selection = document().selection().selection(); |
| 341 | if (!event) |
| 342 | return selection; |
| 343 | // If the target is a text control, and the current selection is outside of its shadow tree, |
| 344 | // then use the saved selection for that text control. |
| 345 | if (RefPtr target = dynamicDowncast<HTMLTextFormControlElement>(event->target()); target && target->isTextField()) { |
| 346 | auto start = selection.start(); |
| 347 | if (start.isNull() || event->target() != enclosingTextFormControl(start)) { |
| 348 | if (auto range = target->selection()) |
| 349 | return { *range, Affinity::Downstream, selection.directionality() }; |
| 350 | } |
| 351 | } |
| 352 | return selection; |
| 353 | } |
| 354 | |
| 355 | // Function considers Mac editing behavior a fallback when Page or Settings is not available. |
| 356 | EditingBehavior Editor::behavior() const |
| 357 | { |
| 358 | return document().editingBehavior(); |
| 359 | } |
| 360 | |
| 361 | EditorClient* Editor::client() const |
| 362 | { |
| 363 | ASSERT(!m_client || !document().page() || m_client == &document().page()->editorClient())((void)0); |
| 364 | return m_client.get(); |
| 365 | } |
| 366 | |
| 367 | TextCheckerClient* Editor::textChecker() const |
| 368 | { |
| 369 | if (CheckedPtr owner = client()) |
| 370 | return owner->textChecker(); |
| 371 | return 0; |
| 372 | } |
| 373 | |
| 374 | void Editor::handleKeyboardEvent(KeyboardEvent& event) |
| 375 | { |
| 376 | if (CheckedPtr client = this->client()) |
| 377 | client->handleKeyboardEvent(event); |
| 378 | } |
| 379 | |
| 380 | void Editor::handleInputMethodKeydown(KeyboardEvent& event) |
| 381 | { |
| 382 | if (CheckedPtr client = this->client()) |
| 383 | client->handleInputMethodKeydown(event); |
| 384 | } |
| 385 | |
| 386 | void Editor::didDispatchInputMethodKeydown(KeyboardEvent& event) |
| 387 | { |
| 388 | if (CheckedPtr client = this->client()) |
| 389 | client->didDispatchInputMethodKeydown(event); |
| 390 | } |
| 391 | |
| 392 | bool Editor::handleTextEvent(TextEvent& event) |
| 393 | { |
| 394 | LOG(Editing, "Editor %p handleTextEvent (data %s)", this, event.data().utf8().data())((void)0); |
| 395 | |
| 396 | // Default event handling for Drag and Drop will be handled by DragController |
| 397 | // so we leave the event for it. |
| 398 | if (event.isDrop()) |
| 399 | return false; |
| 400 | |
| 401 | if (event.isPaste() || event.isRemoveBackground()) { |
| 402 | auto action = event.isRemoveBackground() ? EditAction::RemoveBackground : EditAction::Paste; |
| 403 | if (event.pastingFragment()) { |
| 404 | #if PLATFORM(IOS_FAMILY)(defined 1 && 1) |
| 405 | if (client()->performsTwoStepPaste(event.pastingFragment())) |
| 406 | return true; |
| 407 | #endif |
| 408 | replaceSelectionWithFragment(*event.pastingFragment(), SelectReplacement::No, event.shouldSmartReplace() ? SmartReplace::Yes : SmartReplace::No, event.shouldMatchStyle() ? MatchStyle::Yes : MatchStyle::No, action, event.mailBlockquoteHandling()); |
| 409 | } else |
| 410 | replaceSelectionWithText(event.data(), SelectReplacement::No, event.shouldSmartReplace() ? SmartReplace::Yes : SmartReplace::No, action); |
| 411 | return true; |
| 412 | } |
| 413 | |
| 414 | String data = event.data(); |
| 415 | if (data == "\n"_s) { |
| 416 | if (event.isLineBreak()) |
| 417 | return insertLineBreak(); |
| 418 | return insertParagraphSeparator(); |
| 419 | } |
| 420 | |
| 421 | return insertTextWithoutSendingTextEvent(data, false, &event); |
| 422 | } |
| 423 | |
| 424 | bool Editor::canEdit() const |
| 425 | { |
| 426 | return document().selection().selection().rootEditableElement(); |
| 427 | } |
| 428 | |
| 429 | bool Editor::canEditRichly() const |
| 430 | { |
| 431 | return document().selection().selection().isContentRichlyEditable(); |
| 432 | } |
| 433 | |
| 434 | static AtomString eventNameForClipboardEvent(ClipboardEventKind kind) |
| 435 | { |
| 436 | switch (kind) { |
| 437 | case ClipboardEventKind::Copy: |
| 438 | case ClipboardEventKind::CopyFont: |
| 439 | return eventNames().copyEvent; |
| 440 | case ClipboardEventKind::Cut: |
| 441 | return eventNames().cutEvent; |
| 442 | case ClipboardEventKind::Paste: |
| 443 | case ClipboardEventKind::PasteAsPlainText: |
| 444 | case ClipboardEventKind::PasteAsQuotation: |
| 445 | case ClipboardEventKind::PasteFont: |
| 446 | return eventNames().pasteEvent; |
| 447 | case ClipboardEventKind::BeforeCopy: |
| 448 | return eventNames().beforecopyEvent; |
| 449 | case ClipboardEventKind::BeforeCut: |
| 450 | return eventNames().beforecutEvent; |
| 451 | case ClipboardEventKind::BeforePaste: |
| 452 | return eventNames().beforepasteEvent; |
| 453 | } |
| 454 | ASSERT_NOT_REACHED()((void)0); |
| 455 | return { }; |
| 456 | } |
| 457 | |
| 458 | static Ref<DataTransfer> createDataTransferForClipboardEvent(Document& document, ClipboardEventKind kind) |
| 459 | { |
| 460 | switch (kind) { |
| 461 | case ClipboardEventKind::Copy: |
| 462 | case ClipboardEventKind::CopyFont: |
| 463 | case ClipboardEventKind::Cut: |
| 464 | return DataTransfer::createForCopyAndPaste(document, DataTransfer::StoreMode::ReadWrite, makeUnique<StaticPasteboard>()); |
| 465 | case ClipboardEventKind::PasteAsPlainText: |
| 466 | if (DeprecatedGlobalSettings::customPasteboardDataEnabled()) { |
| 467 | auto plainTextType = textPlainContentTypeAtom(); |
| 468 | auto plainText = Pasteboard::createForCopyAndPaste(PagePasteboardContext::create(document.pageID()))->readString(plainTextType); |
| 469 | auto pasteboard = makeUnique<StaticPasteboard>(); |
| 470 | pasteboard->writeString(plainTextType, plainText); |
| 471 | return DataTransfer::createForCopyAndPaste(document, DataTransfer::StoreMode::Readonly, WTF::move(pasteboard)); |
| 472 | } |
| 473 | [[fallthrough]]; |
| 474 | case ClipboardEventKind::Paste: |
| 475 | case ClipboardEventKind::PasteAsQuotation: |
| 476 | case ClipboardEventKind::PasteFont: |
| 477 | return DataTransfer::createForCopyAndPaste(document, DataTransfer::StoreMode::Readonly, Pasteboard::createForCopyAndPaste(PagePasteboardContext::create(document.pageID()))); |
| 478 | case ClipboardEventKind::BeforeCopy: |
| 479 | case ClipboardEventKind::BeforeCut: |
| 480 | case ClipboardEventKind::BeforePaste: |
| 481 | return DataTransfer::createForCopyAndPaste(document, DataTransfer::StoreMode::Invalid, makeUnique<StaticPasteboard>()); |
| 482 | } |
| 483 | ASSERT_NOT_REACHED()((void)0); |
| 484 | return DataTransfer::createForCopyAndPaste(document, DataTransfer::StoreMode::Invalid, makeUnique<StaticPasteboard>()); |
| 485 | } |
| 486 | |
| 487 | // Returns whether caller should continue with "the default processing", which is the same as |
| 488 | // the event handler NOT setting the return value to false |
| 489 | // https://w3c.github.io/clipboard-apis/#fire-a-clipboard-event |
| 490 | bool Editor::dispatchClipboardEvent(RefPtr<Element>&& target, ClipboardEventKind kind, Ref<DataTransfer>&& dataTransfer) |
| 491 | { |
| 492 | // FIXME: Move the target selection code here. |
| 493 | if (!target) |
| 494 | return true; |
| 495 | |
| 496 | auto event = ClipboardEvent::create(eventNameForClipboardEvent(kind), dataTransfer.copyRef()); |
| 497 | |
| 498 | target->dispatchEvent(event); |
| 499 | bool noDefaultProcessing = event->defaultPrevented(); |
| 500 | if (noDefaultProcessing && (kind == ClipboardEventKind::Copy || kind == ClipboardEventKind::Cut) && dataTransfer->pasteboard().hasData()) |
| 501 | dataTransfer->commitToPasteboard(*Pasteboard::createForCopyAndPaste(PagePasteboardContext::create(target->document().pageID()))); |
| 502 | |
| 503 | dataTransfer->makeInvalidForSecurity(); |
| 504 | |
| 505 | return !noDefaultProcessing; |
| 506 | } |
| 507 | |
| 508 | bool Editor::dispatchClipboardEvent(RefPtr<Element>&& target, ClipboardEventKind kind) |
| 509 | { |
| 510 | if (!target) |
| 511 | return true; |
| 512 | |
| 513 | return Editor::dispatchClipboardEvent(WTF::move(target), kind, createDataTransferForClipboardEvent(target->document(), kind)); |
| 514 | } |
| 515 | |
| 516 | // WinIE uses onbeforecut and onbeforepaste to enables the cut and paste menu items. They |
| 517 | // also send onbeforecopy, apparently for symmetry, but it doesn't affect the menu items. |
| 518 | // We need to use onbeforecopy as a real menu enabler because we allow elements that are not |
| 519 | // normally selectable to implement copy/paste (like divs, or a document body). |
| 520 | |
| 521 | bool Editor::canDHTMLCut() |
| 522 | { |
| 523 | if (document().selection().selection().isInPasswordField()) |
| 524 | return false; |
| 525 | |
| 526 | return !dispatchClipboardEvent(findEventTargetFromSelection(), ClipboardEventKind::BeforeCut); |
| 527 | } |
| 528 | |
| 529 | bool Editor::canDHTMLCopy() |
| 530 | { |
| 531 | if (document().selection().selection().isInPasswordField()) |
| 532 | return false; |
| 533 | return !dispatchClipboardEvent(findEventTargetFromSelection(), ClipboardEventKind::BeforeCopy); |
| 534 | } |
| 535 | |
| 536 | bool Editor::canDHTMLPaste() |
| 537 | { |
| 538 | return !dispatchClipboardEvent(findEventTargetFromSelection(), ClipboardEventKind::BeforePaste); |
| 539 | } |
| 540 | |
| 541 | bool Editor::canCut() const |
| 542 | { |
| 543 | return canCopy() && canDelete(); |
| 544 | } |
| 545 | |
| 546 | static HTMLImageElement* imageElementFromImageDocument(Document& document) |
| 547 | { |
| 548 | if (!document.isImageDocument()) |
| 549 | return nullptr; |
| 550 | |
| 551 | RefPtr body = document.bodyOrFrameset(); |
| 552 | return body ? dynamicDowncast<HTMLImageElement>(body->firstChild()) : nullptr; |
| 553 | } |
| 554 | |
| 555 | bool Editor::canCopy() const |
| 556 | { |
| 557 | if (imageElementFromImageDocument(document())) |
| 558 | return true; |
| 559 | const VisibleSelection& selection = document().selection().selection(); |
| 560 | return (selection.isRange() || !isEditablePosition(selection.start())) && (!selection.isInPasswordField() || selection.isInAutoFilledAndViewableField()); |
| 561 | } |
| 562 | |
| 563 | bool Editor::canDelete() const |
| 564 | { |
| 565 | const VisibleSelection& selection = document().selection().selection(); |
| 566 | return selection.isRange() && selection.rootEditableElement(); |
| 567 | } |
| 568 | |
| 569 | bool Editor::canDeleteRange(const SimpleRange& range) const |
| 570 | { |
| 571 | if (!range.start.container->hasEditableStyle() || !range.end.container->hasEditableStyle()) |
| 572 | return false; |
| 573 | |
| 574 | if (range.collapsed()) { |
| 575 | // FIXME: We sometimes allow deletions at the start of editable roots, like when the caret is in an empty list item. |
| 576 | auto previous = VisiblePosition { makeDeprecatedLegacyPosition(range.start) }.previous(); |
| 577 | if (previous.isNull() || previous.deepEquivalent().deprecatedNode()->rootEditableElement() != range.start.container->rootEditableElement()) |
| 578 | return false; |
| 579 | } |
| 580 | |
| 581 | return true; |
| 582 | } |
| 583 | |
| 584 | bool Editor::shouldSmartDelete() |
| 585 | { |
| 586 | if (behavior().shouldAlwaysSmartDelete()) |
| 587 | return true; |
| 588 | return document().selection().granularity() == TextGranularity::WordGranularity; |
| 589 | } |
| 590 | |
| 591 | bool Editor::smartInsertDeleteEnabled() |
| 592 | { |
| 593 | return client() && client()->smartInsertDeleteEnabled(); |
| 594 | } |
| 595 | |
| 596 | bool Editor::canSmartCopyOrDelete() |
| 597 | { |
| 598 | return client() && client()->smartInsertDeleteEnabled() && shouldSmartDelete(); |
| 599 | } |
| 600 | |
| 601 | bool Editor::isSelectTrailingWhitespaceEnabled() const |
| 602 | { |
| 603 | return client() && client()->isSelectTrailingWhitespaceEnabled(); |
| 604 | } |
| 605 | |
| 606 | bool Editor::deleteWithDirection(SelectionDirection direction, TextGranularity granularity, bool shouldAddToKillRing, bool isTypingAction) |
| 607 | { |
| 608 | if (!canEdit()) |
| 609 | return false; |
| 610 | |
| 611 | Ref document = this->document(); |
| 612 | if (document->selection().isRange()) { |
| 613 | if (isTypingAction) { |
| 614 | TypingCommand::deleteKeyPressed(WTF::move(document), canSmartCopyOrDelete() ? TypingCommand::Option::SmartDelete : OptionSet<TypingCommand::Option> { }, granularity); |
| 615 | revealSelectionAfterEditingOperation(); |
| 616 | } else { |
| 617 | if (shouldAddToKillRing) |
| 618 | addRangeToKillRing(*selectedRange(), KillRingInsertionMode::AppendText); |
| 619 | deleteSelectionWithSmartDelete(canSmartCopyOrDelete()); |
| 620 | // Implicitly calls revealSelectionAfterEditingOperation(). |
| 621 | } |
| 622 | } else { |
| 623 | OptionSet<TypingCommand::Option> options; |
| 624 | if (canSmartCopyOrDelete()) |
| 625 | options.add(TypingCommand::Option::SmartDelete); |
| 626 | if (shouldAddToKillRing) |
| 627 | options.add(TypingCommand::Option::AddsToKillRing); |
| 628 | switch (direction) { |
| 629 | case SelectionDirection::Forward: |
| 630 | case SelectionDirection::Right: |
| 631 | TypingCommand::forwardDeleteKeyPressed(WTF::move(document), options, granularity); |
| 632 | break; |
| 633 | case SelectionDirection::Backward: |
| 634 | case SelectionDirection::Left: |
| 635 | TypingCommand::deleteKeyPressed(WTF::move(document), options, granularity); |
| 636 | break; |
| 637 | } |
| 638 | revealSelectionAfterEditingOperation(); |
| 639 | } |
| 640 | |
| 641 | // FIXME: We should to move this down into deleteKeyPressed. |
| 642 | // clear the "start new kill ring sequence" setting, because it was set to true |
| 643 | // when the selection was updated by deleting the range |
| 644 | if (shouldAddToKillRing) |
| 645 | setStartNewKillRingSequence(false); |
| 646 | |
| 647 | return true; |
| 648 | } |
| 649 | |
| 650 | void Editor::deleteSelectionWithSmartDelete(bool smartDelete, EditAction editingAction) |
| 651 | { |
| 652 | Ref document = this->document(); |
| 653 | if (document->selection().isNone()) |
| 654 | return; |
| 655 | |
| 656 | DeleteSelectionCommand::create(WTF::move(document), smartDelete, true, false, false, true, editingAction)->apply(); |
| 657 | } |
| 658 | |
| 659 | void Editor::clearText() |
| 660 | { |
| 661 | ClearTextCommand::CreateAndApply(protect(document())); |
| 662 | } |
| 663 | |
| 664 | void Editor::pasteAsPlainText(const String& pastingText, bool smartReplace) |
| 665 | { |
| 666 | auto target = findEventTargetFromSelection(); |
| 667 | if (!target) |
| 668 | return; |
| 669 | auto sanitizedText = pastingText; |
| 670 | Ref document = this->document(); |
| 671 | if (RefPtr page = document->page()) |
| 672 | sanitizedText = page->applyLinkDecorationFiltering(sanitizedText, LinkDecorationFilteringTrigger::Paste); |
| 673 | target->dispatchEvent(TextEvent::createForPlainTextPaste(document->windowProxy(), WTF::move(sanitizedText), smartReplace)); |
| 674 | } |
| 675 | |
| 676 | void Editor::pasteAsFragment(Ref<DocumentFragment>&& pastingFragment, bool smartReplace, bool matchStyle, MailBlockquoteHandling respectsMailBlockquote, EditAction action) |
| 677 | { |
| 678 | auto target = findEventTargetFromSelection(); |
| 679 | if (!target) |
| 680 | return; |
| 681 | |
| 682 | ASSERT(action == EditAction::RemoveBackground || action == EditAction::Paste)((void)0); |
| 683 | auto type = action == EditAction::RemoveBackground ? TextEventInputRemoveBackground : TextEventInputPaste; |
| 684 | target->dispatchEvent(TextEvent::createForFragmentPaste(document().windowProxy(), WTF::move(pastingFragment), type, smartReplace, matchStyle, respectsMailBlockquote)); |
| 685 | } |
| 686 | |
| 687 | void Editor::pasteAsPlainTextBypassingDHTML() |
| 688 | { |
| 689 | pasteAsPlainTextWithPasteboard(*Pasteboard::createForCopyAndPaste(PagePasteboardContext::create(document().pageID()))); |
| 690 | } |
| 691 | |
| 692 | void Editor::pasteAsPlainTextWithPasteboard(Pasteboard& pasteboard) |
| 693 | { |
| 694 | String text = readPlainTextFromPasteboard(pasteboard); |
| 695 | if (client() && client()->shouldInsertText(text, selectedRange(), EditorInsertAction::Pasted)) |
| 696 | pasteAsPlainText(text, canSmartReplaceWithPasteboard(pasteboard)); |
| 697 | } |
| 698 | |
| 699 | String Editor::readPlainTextFromPasteboard(Pasteboard& pasteboard) |
| 700 | { |
| 701 | PasteboardPlainText text; |
| 702 | pasteboard.read(text); |
| 703 | return plainTextFromPasteboard(text); |
| 704 | } |
| 705 | |
| 706 | #if !PLATFORM(MAC)(defined WTF_PLATFORM_MAC && WTF_PLATFORM_MAC) |
| 707 | |
| 708 | String Editor::plainTextFromPasteboard(const PasteboardPlainText& text) |
| 709 | { |
| 710 | return text.text; |
| 711 | } |
| 712 | |
| 713 | #endif |
| 714 | |
| 715 | bool Editor::canSmartReplaceWithPasteboard(Pasteboard& pasteboard) |
| 716 | { |
| 717 | return client() && client()->smartInsertDeleteEnabled() && pasteboard.canSmartReplace(); |
| 718 | } |
| 719 | |
| 720 | bool Editor::shouldInsertFragment(DocumentFragment& fragment, const std::optional<SimpleRange>& replacingDOMRange, EditorInsertAction givenAction) |
| 721 | { |
| 722 | if (!client()) |
| 723 | return false; |
| 724 | |
| 725 | if (RefPtr child = dynamicDowncast<CharacterData>(fragment.firstChild()); child && fragment.lastChild() == fragment.firstChild()) |
| 726 | return client()->shouldInsertText(child->data(), replacingDOMRange, givenAction); |
| 727 | |
| 728 | return client()->shouldInsertNode(fragment, replacingDOMRange, givenAction); |
| 729 | } |
| 730 | |
| 731 | void Editor::replaceSelectionWithFragment(DocumentFragment& fragment, SelectReplacement selectReplacement, SmartReplace smartReplace, MatchStyle matchStyle, EditAction editingAction, MailBlockquoteHandling mailBlockquoteHandling) |
| 732 | { |
| 733 | Ref document = this->document(); |
| 734 | VisibleSelection selection = document->selection().selection(); |
| 735 | if (selection.isNone() || !selection.isContentEditable()) |
| 736 | return; |
| 737 | |
| 738 | AccessibilityReplacedText replacedText; |
| 739 | if (AXObjectCache::accessibilityEnabled() && (editingAction == EditAction::Paste || editingAction == EditAction::Insert || editingAction == EditAction::RemoveBackground)) |
| 740 | replacedText = AccessibilityReplacedText(selection); |
| 741 | |
| 742 | OptionSet<ReplaceSelectionCommand::CommandOption> options { ReplaceSelectionCommand::PreventNesting, ReplaceSelectionCommand::SanitizeFragment }; |
| 743 | if (selectReplacement == SelectReplacement::Yes) |
| 744 | options.add(ReplaceSelectionCommand::SelectReplacement); |
| 745 | if (smartReplace == SmartReplace::Yes) |
| 746 | options.add(ReplaceSelectionCommand::SmartReplace); |
| 747 | if (matchStyle == MatchStyle::Yes) |
| 748 | options.add(ReplaceSelectionCommand::MatchStyle); |
| 749 | if (mailBlockquoteHandling == MailBlockquoteHandling::IgnoreBlockquote) |
| 750 | options.add(ReplaceSelectionCommand::IgnoreMailBlockquote); |
| 751 | |
| 752 | auto command = ReplaceSelectionCommand::create(document.copyRef(), &fragment, options, editingAction); |
| 753 | command->apply(); |
| 754 | |
| 755 | m_imageElementsToLoadBeforeRevealingSelection.clear(); |
| 756 | if (auto insertionRange = command->insertedContentRange()) |
| 757 | m_imageElementsToLoadBeforeRevealingSelection = visibleImageElementsInRangeWithNonLoadedImages(*insertionRange); |
| 758 | |
| 759 | if (m_imageElementsToLoadBeforeRevealingSelection.isEmpty()) |
| 760 | revealSelectionAfterEditingOperation(); |
| 761 | |
| 762 | selection = document->selection().selection(); |
| 763 | if (selection.isInPasswordField()) |
| 764 | return; |
| 765 | |
| 766 | if (AXObjectCache::accessibilityEnabled() && editingAction == EditAction::Paste) { |
| 767 | String text = AccessibilityObject::stringForVisiblePositionRange(command->visibleSelectionForInsertedText()); |
| 768 | replacedText.postTextStateChangeNotification(document->existingAXObjectCache(), AXTextEditType::Paste, text, document->selection().selection()); |
| 769 | command->composition()->setRangeDeletedByUnapply(replacedText.replacedRange()); |
| 770 | } |
| 771 | |
| 772 | if (AXObjectCache::accessibilityEnabled() && editingAction == EditAction::Insert) { |
| 773 | String text = command->documentFragmentPlainText(); |
| 774 | replacedText.postTextStateChangeNotification(document->existingAXObjectCache(), AXTextEditType::Insert, text, document->selection().selection()); |
| 775 | command->composition()->setRangeDeletedByUnapply(replacedText.replacedRange()); |
| 776 | } |
| 777 | |
| 778 | if (!isContinuousSpellCheckingEnabled()) |
| 779 | return; |
| 780 | |
| 781 | RefPtr nodeToCheck { selection.rootEditableElement() }; |
| 782 | if (!nodeToCheck) |
| 783 | return; |
| 784 | |
| 785 | auto rangeToCheck = makeRangeSelectingNodeContents(*nodeToCheck); |
| 786 | if (auto request = SpellCheckRequest::create(resolveTextCheckingTypeMask(*nodeToCheck, { TextCheckingType::Spelling, TextCheckingType::Grammar }), TextCheckingProcessType::TextCheckingProcessBatch, rangeToCheck, rangeToCheck, rangeToCheck)) |
| 787 | m_spellChecker->requestCheckingFor(request.releaseNonNull()); |
| 788 | } |
| 789 | |
| 790 | void Editor::replaceSelectionWithText(const String& text, SelectReplacement selectReplacement, SmartReplace smartReplace, EditAction editingAction) |
| 791 | { |
| 792 | auto range = selectedRange(); |
| 793 | if (!range) |
| 794 | return; |
| 795 | |
| 796 | replaceSelectionWithFragment(createFragmentFromText(*range, text), selectReplacement, smartReplace, MatchStyle::Yes, editingAction); |
| 797 | } |
| 798 | |
| 799 | std::optional<SimpleRange> Editor::selectedRange() |
| 800 | { |
| 801 | return document().selection().selection().toNormalizedRange(); |
| 802 | } |
| 803 | |
| 804 | bool Editor::shouldDeleteRange(const std::optional<SimpleRange>& range) const |
| 805 | { |
| 806 | return range && !range->collapsed() && canDeleteRange(*range) && client() && client()->shouldDeleteRange(*range); |
| 807 | } |
| 808 | |
| 809 | bool Editor::tryDHTMLCopy() |
| 810 | { |
| 811 | if (document().selection().selection().isInPasswordField()) |
| 812 | return false; |
| 813 | |
| 814 | return !dispatchClipboardEvent(findEventTargetFromSelection(), ClipboardEventKind::Copy); |
| 815 | } |
| 816 | |
| 817 | bool Editor::tryDHTMLCut() |
| 818 | { |
| 819 | if (document().selection().selection().isInPasswordField()) |
| 820 | return false; |
| 821 | |
| 822 | return !dispatchClipboardEvent(findEventTargetFromSelection(), ClipboardEventKind::Cut); |
| 823 | } |
| 824 | |
| 825 | bool Editor::shouldInsertText(const String& text, const std::optional<SimpleRange>& range, EditorInsertAction action) const |
| 826 | { |
| 827 | // FIXME(273431): shouldSuppressTextInputFromEditing does not work with site isolation. |
| 828 | RefPtr localFrame = document().localMainFrame(); |
| 829 | if (localFrame && localFrame->loader().shouldSuppressTextInputFromEditing() && action == EditorInsertAction::Typed) |
| 830 | return false; |
| 831 | |
| 832 | return client() && client()->shouldInsertText(text, range, action); |
| 833 | } |
| 834 | |
| 835 | void Editor::respondToChangedContents(const VisibleSelection& endingSelection) |
| 836 | { |
| 837 | if (AXObjectCache::accessibilityEnabled()) { |
| 838 | if (RefPtr node = endingSelection.start().deprecatedNode()) { |
| 839 | if (CheckedPtr cache = document().existingAXObjectCache()) |
| 840 | cache->onEditableTextValueChanged(*node.get()); |
| 841 | } |
| 842 | } |
| 843 | |
| 844 | updateMarkersForWordsAffectedByEditing(true); |
| 845 | |
| 846 | if (client()) |
| 847 | client()->respondToChangedContents(); |
| 848 | } |
| 849 | |
| 850 | bool Editor::hasBidiSelection() const |
| 851 | { |
| 852 | Ref document = this->document(); |
| 853 | if (document->selection().isNone()) |
| 854 | return false; |
| 855 | |
| 856 | RefPtr<Node> startNode; |
| 857 | if (document->selection().isRange()) { |
| 858 | startNode = document->selection().selection().start().downstream().deprecatedNode(); |
| 859 | RefPtr endNode = document->selection().selection().end().upstream().deprecatedNode(); |
| 860 | if (enclosingBlock(startNode.get()) != enclosingBlock(endNode.get())) |
| 861 | return false; |
| 862 | } else |
| 863 | startNode = document->selection().selection().visibleStart().deepEquivalent().deprecatedNode(); |
| 864 | |
| 865 | if (!startNode || !startNode->renderer()) |
| 866 | return false; |
| 867 | |
| 868 | ScriptDisallowedScope::InMainThread scriptDisallowedScope; |
| 869 | |
| 870 | if (CheckedPtr renderBlockFlow = ancestorsOfType<RenderBlockFlow>(*startNode->renderer()).first()) |
| 871 | return !renderBlockFlow->style().writingMode().deprecatedIsLeftToRightDirection() || renderBlockFlow->containsNonZeroBidiLevel(); |
| 872 | return false; |
| 873 | } |
| 874 | |
| 875 | TriState Editor::selectionUnorderedListState() const |
| 876 | { |
| 877 | Ref document = this->document(); |
| 878 | if (document->selection().isCaret()) { |
| 879 | if (enclosingElementWithTag(document->selection().selection().start(), ulTag)) |
| 880 | return TriState::True; |
| 881 | } else if (document->selection().isRange()) { |
| 882 | RefPtr startNode { enclosingElementWithTag(document->selection().selection().start(), ulTag) }; |
| 883 | RefPtr endNode { enclosingElementWithTag(document->selection().selection().end(), ulTag) }; |
| 884 | if (startNode && endNode && startNode == endNode) |
| 885 | return TriState::True; |
| 886 | } |
| 887 | |
| 888 | return TriState::False; |
| 889 | } |
| 890 | |
| 891 | TriState Editor::selectionOrderedListState() const |
| 892 | { |
| 893 | Ref document = this->document(); |
| 894 | if (document->selection().isCaret()) { |
| 895 | if (enclosingElementWithTag(document->selection().selection().start(), olTag)) |
| 896 | return TriState::True; |
| 897 | } else if (document->selection().isRange()) { |
| 898 | RefPtr startNode { enclosingElementWithTag(document->selection().selection().start(), olTag) }; |
| 899 | RefPtr endNode { enclosingElementWithTag(document->selection().selection().end(), olTag) }; |
| 900 | if (startNode && endNode && startNode == endNode) |
| 901 | return TriState::True; |
| 902 | } |
| 903 | |
| 904 | return TriState::False; |
| 905 | } |
| 906 | |
| 907 | RefPtr<Node> Editor::insertOrderedList() |
| 908 | { |
| 909 | if (!canEditRichly()) |
| 910 | return nullptr; |
| 911 | |
| 912 | auto newList = InsertListCommand::insertList(protect(document()), InsertListCommand::Type::OrderedList); |
| 913 | revealSelectionAfterEditingOperation(); |
| 914 | return newList; |
| 915 | } |
| 916 | |
| 917 | RefPtr<Node> Editor::insertUnorderedList() |
| 918 | { |
| 919 | if (!canEditRichly()) |
| 920 | return nullptr; |
| 921 | |
| 922 | auto newList = InsertListCommand::insertList(protect(document()), InsertListCommand::Type::UnorderedList); |
| 923 | revealSelectionAfterEditingOperation(); |
| 924 | return newList; |
| 925 | } |
| 926 | |
| 927 | bool Editor::canIncreaseSelectionListLevel() |
| 928 | { |
| 929 | return canEditRichly() && IncreaseSelectionListLevelCommand::canIncreaseSelectionListLevel(&document()); |
| 930 | } |
| 931 | |
| 932 | bool Editor::canDecreaseSelectionListLevel() |
| 933 | { |
| 934 | return canEditRichly() && DecreaseSelectionListLevelCommand::canDecreaseSelectionListLevel(&document()); |
| 935 | } |
| 936 | |
| 937 | RefPtr<Node> Editor::increaseSelectionListLevel() |
| 938 | { |
| 939 | Ref document = this->document(); |
| 940 | if (!canEditRichly() || document->selection().isNone()) |
| 941 | return nullptr; |
| 942 | |
| 943 | RefPtr<Node> newList = IncreaseSelectionListLevelCommand::increaseSelectionListLevel(document.ptr()); |
| 944 | revealSelectionAfterEditingOperation(); |
| 945 | return newList; |
| 946 | } |
| 947 | |
| 948 | RefPtr<Node> Editor::increaseSelectionListLevelOrdered() |
| 949 | { |
| 950 | Ref document = this->document(); |
| 951 | if (!canEditRichly() || document->selection().isNone()) |
| 952 | return nullptr; |
| 953 | |
| 954 | RefPtr<Node> newList = IncreaseSelectionListLevelCommand::increaseSelectionListLevelOrdered(document.ptr()); |
| 955 | revealSelectionAfterEditingOperation(); |
| 956 | return newList; |
| 957 | } |
| 958 | |
| 959 | RefPtr<Node> Editor::increaseSelectionListLevelUnordered() |
| 960 | { |
| 961 | Ref document = this->document(); |
| 962 | if (!canEditRichly() || document->selection().isNone()) |
| 963 | return nullptr; |
| 964 | |
| 965 | RefPtr<Node> newList = IncreaseSelectionListLevelCommand::increaseSelectionListLevelUnordered(document.ptr()); |
| 966 | revealSelectionAfterEditingOperation(); |
| 967 | return newList; |
| 968 | } |
| 969 | |
| 970 | void Editor::decreaseSelectionListLevel() |
| 971 | { |
| 972 | Ref document = this->document(); |
| 973 | if (!canEditRichly() || document->selection().isNone()) |
| 974 | return; |
| 975 | |
| 976 | DecreaseSelectionListLevelCommand::decreaseSelectionListLevel(document.ptr()); |
| 977 | revealSelectionAfterEditingOperation(); |
| 978 | } |
| 979 | |
| 980 | void Editor::removeFormattingAndStyle() |
| 981 | { |
| 982 | RemoveFormatCommand::create(protect(document()))->apply(); |
| 983 | } |
| 984 | |
| 985 | void Editor::clearLastEditCommand() |
| 986 | { |
| 987 | m_lastEditCommand = nullptr; |
| 988 | } |
| 989 | |
| 990 | RefPtr<Element> Editor::findEventTargetFrom(const VisibleSelection& selection) const |
| 991 | { |
| 992 | RefPtr target { selection.start().anchorElementAncestor() }; |
| 993 | if (!target) |
| 994 | target = document().bodyOrFrameset(); |
| 995 | |
| 996 | return target; |
| 997 | } |
| 998 | |
| 999 | RefPtr<Element> Editor::findEventTargetFromSelection() const |
| 1000 | { |
| 1001 | // https://www.w3.org/TR/clipboard-apis/#fire-a-clipboard-event says: |
| 1002 | // If the context is editable, then |
| 1003 | // Set target to be the element that contains the start of the visible selection or cursor in document order, or the body element if there is no visible selection or cursor. |
| 1004 | const VisibleSelection& selection = document().selection().selection(); |
| 1005 | if (selection.isRange() || isEditablePosition(selection.start())) |
| 1006 | return findEventTargetFrom(selection); |
| 1007 | // Else, if the context is not editable, then |
| 1008 | // Set target to the focused node, or the body element if no node has focus. |
| 1009 | return document().activeElement(); |
| 1010 | } |
| 1011 | |
| 1012 | void Editor::applyStyle(StyleProperties* style, EditAction editingAction) |
| 1013 | { |
| 1014 | if (style) |
| 1015 | applyStyle(EditingStyle::create(style), editingAction, ColorFilterMode::UseOriginalColor); |
| 1016 | } |
| 1017 | |
| 1018 | void Editor::applyStyle(RefPtr<EditingStyle>&& style, EditAction editingAction, ColorFilterMode colorFilterMode) |
| 1019 | { |
| 1020 | if (!style) |
| 1021 | return; |
| 1022 | |
| 1023 | Ref document = this->document(); |
| 1024 | if (document->selection().isNone()) |
| 1025 | return; |
| 1026 | |
| 1027 | AtomString inputTypeName = inputTypeNameForEditingAction(editingAction); |
| 1028 | auto isInputMethodComposing = isInputMethodComposingForEditingAction(editingAction) ? IsInputMethodComposing::Yes : IsInputMethodComposing::No; |
| 1029 | String inputEventData = inputEventDataForEditingStyleAndAction(*style, editingAction); |
| 1030 | RefPtr element { document->selection().selection().rootEditableElement() }; |
| 1031 | |
| 1032 | if (element && !dispatchBeforeInputEvent(*element, inputTypeName, isInputMethodComposing, inputEventData)) |
| 1033 | return; |
| 1034 | |
| 1035 | if (document->selection().isNone()) |
| 1036 | return; |
| 1037 | |
| 1038 | auto styleToApply = colorFilterMode == ColorFilterMode::InvertColor ? style->inverseTransformColorIfNeeded(*element) : style.releaseNonNull(); |
| 1039 | if (document->selection().isCaret()) |
| 1040 | computeAndSetTypingStyle(styleToApply, editingAction); |
| 1041 | else |
| 1042 | ApplyStyleCommand::create(WTF::move(document), styleToApply.ptr(), editingAction)->apply(); |
| 1043 | |
| 1044 | if (client()) |
| 1045 | client()->didApplyStyle(); |
| 1046 | if (element) |
| 1047 | dispatchInputEvent(*element, inputTypeName, isInputMethodComposing, inputEventData); |
| 1048 | } |
| 1049 | |
| 1050 | bool Editor::shouldApplyStyle(const StyleProperties& style, const SimpleRange& range) |
| 1051 | { |
| 1052 | return client()->shouldApplyStyle(style, range); |
| 1053 | } |
| 1054 | |
| 1055 | void Editor::applyParagraphStyle(StyleProperties* style, EditAction editingAction) |
| 1056 | { |
| 1057 | if (!style) |
| 1058 | return; |
| 1059 | |
| 1060 | Ref document = this->document(); |
| 1061 | if (document->selection().isNone()) |
| 1062 | return; |
| 1063 | |
| 1064 | AtomString inputTypeName = inputTypeNameForEditingAction(editingAction); |
| 1065 | auto isInputMethodComposing = isInputMethodComposingForEditingAction(editingAction) ? IsInputMethodComposing::Yes : IsInputMethodComposing::No; |
| 1066 | String inputEventData = inputEventDataForEditingStyleAndAction(style, editingAction); |
| 1067 | RefPtr element { document->selection().selection().rootEditableElement() }; |
| 1068 | if (element && !dispatchBeforeInputEvent(*element, inputTypeName, isInputMethodComposing, inputEventData)) |
| 1069 | return; |
| 1070 | if (document->selection().isNone()) |
| 1071 | return; |
| 1072 | |
| 1073 | ApplyStyleCommand::create(WTF::move(document), EditingStyle::create(style).ptr(), editingAction, ApplyStylePropertyLevel::ForceBlock)->apply(); |
| 1074 | |
| 1075 | if (client()) |
| 1076 | client()->didApplyStyle(); |
| 1077 | if (element) |
| 1078 | dispatchInputEvent(*element, inputTypeName, isInputMethodComposing, inputEventData); |
| 1079 | } |
| 1080 | |
| 1081 | void Editor::applyStyleToSelection(StyleProperties* style, EditAction editingAction) |
| 1082 | { |
| 1083 | if (!style || style->isEmpty() || !canEditRichly()) |
| 1084 | return; |
| 1085 | |
| 1086 | if (!client() || !client()->shouldApplyStyle(*style, document().selection().selection().toNormalizedRange())) |
| 1087 | return; |
| 1088 | applyStyle(style, editingAction); |
| 1089 | } |
| 1090 | |
| 1091 | void Editor::applyStyleToSelection(Ref<EditingStyle>&& style, EditAction editingAction, ColorFilterMode colorFilterMode) |
| 1092 | { |
| 1093 | if (style->isEmpty() || !canEditRichly()) |
| 1094 | return; |
| 1095 | |
| 1096 | // FIXME: This is wrong for text decorations since m_mutableStyle is empty. |
| 1097 | if (!client() || !client()->shouldApplyStyle(style->styleWithResolvedTextDecorations(), document().selection().selection().toNormalizedRange())) |
| 1098 | return; |
| 1099 | |
| 1100 | applyStyle(WTF::move(style), editingAction, colorFilterMode); |
| 1101 | } |
| 1102 | |
| 1103 | void Editor::applyParagraphStyleToSelection(StyleProperties* style, EditAction editingAction) |
| 1104 | { |
| 1105 | if (!style || style->isEmpty() || !canEditRichly()) |
| 1106 | return; |
| 1107 | |
| 1108 | if (client() && client()->shouldApplyStyle(*style, document().selection().selection().toNormalizedRange())) |
| 1109 | applyParagraphStyle(style, editingAction); |
| 1110 | } |
| 1111 | |
| 1112 | bool Editor::selectionStartHasStyle(CSSPropertyID propertyID, const String& value) const |
| 1113 | { |
| 1114 | if (auto editingStyle = EditingStyle::styleAtSelectionStart(document().selection().selection(), propertyID == CSSPropertyBackgroundColor)) |
| 1115 | return editingStyle->hasStyle(propertyID, value); |
| 1116 | return false; |
| 1117 | } |
| 1118 | |
| 1119 | TriState Editor::selectionHasStyle(CSSPropertyID propertyID, const String& value) const |
| 1120 | { |
| 1121 | return EditingStyle::create(propertyID, value)->triStateOfStyle(document().selection().selection()); |
| 1122 | } |
| 1123 | |
| 1124 | String Editor::selectionStartCSSPropertyValue(CSSPropertyID propertyID) |
| 1125 | { |
| 1126 | Ref document = this->document(); |
| 1127 | RefPtr<EditingStyle> selectionStyle = EditingStyle::styleAtSelectionStart(document->selection().selection(), |
| 1128 | propertyID == CSSPropertyBackgroundColor); |
| 1129 | if (!selectionStyle || !selectionStyle->style()) |
| 1130 | return String(); |
| 1131 | |
| 1132 | if (propertyID == CSSPropertyFontSize) |
| 1133 | return String::number(selectionStyle->legacyFontSize(document)); |
| 1134 | return selectionStyle->style()->getPropertyValue(propertyID); |
| 1135 | } |
| 1136 | |
| 1137 | static void notifyTextFromControls(Element* startRoot, Element* endRoot, bool wasUserEdit = true) |
| 1138 | { |
| 1139 | RefPtr startingTextControl { enclosingTextFormControl(firstPositionInOrBeforeNode(startRoot)) }; |
| 1140 | RefPtr endingTextControl { enclosingTextFormControl(firstPositionInOrBeforeNode(endRoot)) }; |
| 1141 | if (startingTextControl) |
| 1142 | startingTextControl->didEditInnerTextValue(wasUserEdit); |
| 1143 | if (endingTextControl && startingTextControl != endingTextControl) |
| 1144 | endingTextControl->didEditInnerTextValue(wasUserEdit); |
| 1145 | } |
| 1146 | |
| 1147 | static inline bool shouldRemoveAutocorrectionIndicator(bool shouldConsiderApplyingAutocorrection, bool autocorrectionWasApplied, bool isAutocompletion) |
| 1148 | { |
| 1149 | #if HAVE(AUTOCORRECTION_ENHANCEMENTS)(defined 1 && 1) |
| 1150 | bool shouldRemoveIndicator = shouldConsiderApplyingAutocorrection && !autocorrectionWasApplied; |
| 1151 | #if PLATFORM(IOS_FAMILY)(defined 1 && 1) |
| 1152 | // On iOS, unlike macOS, autocorrection is applied as two separate text insertions. The first insertion is |
| 1153 | // an autocompletion. |
| 1154 | return shouldRemoveIndicator && !isAutocompletion; |
| 1155 | #else // !PLATFORM(IOS_FAMILY) |
| 1156 | UNUSED_PARAM(isAutocompletion)(void)isAutocompletion; |
| 1157 | return shouldRemoveIndicator; |
| 1158 | #endif // !PLATFORM(IOS_FAMILY) |
| 1159 | #else // !HAVE(AUTOCORRECTION_ENHANCEMENTS) |
| 1160 | UNUSED_PARAM(shouldConsiderApplyingAutocorrection)(void)shouldConsiderApplyingAutocorrection; |
| 1161 | UNUSED_PARAM(isAutocompletion)(void)isAutocompletion; |
| 1162 | #endif // !HAVE(AUTOCORRECTION_ENHANCEMENTS) |
| 1163 | return !autocorrectionWasApplied; |
| 1164 | } |
| 1165 | |
| 1166 | static inline bool didApplyAutocorrection(Document& document, AlternativeTextController& alternativeTextController) |
| 1167 | { |
| 1168 | #if HAVE(AUTOCORRECTION_ENHANCEMENTS)(defined 1 && 1) && PLATFORM(IOS_FAMILY)(defined 1 && 1) |
| 1169 | bool autocorrectionWasApplied = alternativeTextController.applyAutocorrectionBeforeTypingIfAppropriate(); |
| 1170 | |
| 1171 | // On iOS, unlike macOS, autocorrection is applied as two separate text insertions: the correction |
| 1172 | // itself, followed by a space. This logic detects that an autocorrection was applied after the |
| 1173 | // space has been inserted. |
| 1174 | |
| 1175 | auto selection = document.selection().selection(); |
| 1176 | auto startOfSelection = selection.start(); |
| 1177 | |
| 1178 | auto wordStart = startOfWord(startOfSelection, WordSide::LeftWordIfOnBoundary); |
| 1179 | auto wordEnd = endOfWord(startOfSelection, WordSide::LeftWordIfOnBoundary); |
| 1180 | |
| 1181 | if (auto range = makeSimpleRange(wordStart, wordEnd)) { |
| 1182 | if (CheckedPtr markers = document.markersIfExists(); markers && markers->hasMarkers(*range, DocumentMarkerType::CorrectionIndicator)) |
| 1183 | autocorrectionWasApplied = true; |
| 1184 | } |
| 1185 | |
| 1186 | return autocorrectionWasApplied; |
| 1187 | #else |
| 1188 | UNUSED_PARAM(document)(void)document; |
| 1189 | return alternativeTextController.applyAutocorrectionBeforeTypingIfAppropriate(); |
| 1190 | #endif |
| 1191 | } |
| 1192 | |
| 1193 | static inline void adjustMarkerTypesToRemoveForWordsAffectedByEditing(OptionSet<DocumentMarkerType>& markerTypes) |
| 1194 | { |
| 1195 | #if HAVE(AUTOCORRECTION_ENHANCEMENTS)(defined 1 && 1) && PLATFORM(IOS_FAMILY)(defined 1 && 1) |
| 1196 | markerTypes.remove(DocumentMarkerType::CorrectionIndicator); |
| 1197 | #else |
| 1198 | UNUSED_PARAM(markerTypes)(void)markerTypes; |
| 1199 | #endif |
| 1200 | } |
| 1201 | |
| 1202 | static bool dispatchBeforeInputEvents(RefPtr<Element> startRoot, RefPtr<Element> endRoot, const AtomString& inputTypeName, IsInputMethodComposing isInputMethodComposing, |
| 1203 | const String& data = { }, RefPtr<DataTransfer>&& dataTransfer = nullptr, const Vector<Ref<StaticRange>>& targetRanges = { }, Event::IsCancelable cancelable = Event::IsCancelable::Yes) |
| 1204 | { |
| 1205 | bool continueWithDefaultBehavior = true; |
| 1206 | if (startRoot) |
| 1207 | continueWithDefaultBehavior &= dispatchBeforeInputEvent(*startRoot, inputTypeName, isInputMethodComposing, data, WTF::move(dataTransfer), targetRanges, cancelable); |
| 1208 | if (endRoot && endRoot != startRoot) |
| 1209 | continueWithDefaultBehavior &= dispatchBeforeInputEvent(*endRoot, inputTypeName, isInputMethodComposing, data, WTF::move(dataTransfer), targetRanges, cancelable); |
| 1210 | return continueWithDefaultBehavior; |
| 1211 | } |
| 1212 | |
| 1213 | static void dispatchInputEvents(RefPtr<Element> startRoot, RefPtr<Element> endRoot, const AtomString& inputTypeName, IsInputMethodComposing isInputMethodComposing, |
| 1214 | const String& data = { }, RefPtr<DataTransfer>&& dataTransfer = nullptr, const Vector<Ref<StaticRange>>& targetRanges = { }) |
| 1215 | { |
| 1216 | if (startRoot) |
| 1217 | dispatchInputEvent(*startRoot, inputTypeName, isInputMethodComposing, data, WTF::move(dataTransfer), targetRanges); |
| 1218 | if (endRoot && endRoot != startRoot) |
| 1219 | dispatchInputEvent(*endRoot, inputTypeName, isInputMethodComposing, data, WTF::move(dataTransfer), targetRanges); |
| 1220 | } |
| 1221 | |
| 1222 | bool Editor::willApplyEditing(CompositeEditCommand& command, Vector<Ref<StaticRange>>&& targetRanges) |
| 1223 | { |
| 1224 | #if ENABLE(WRITING_TOOLS)(defined 1 && 1) |
| 1225 | if (suppressEditingForWritingTools()) { |
| 1226 | RELEASE_LOG(Editing, "Editor %p suppressed editing for Writing Tools", this)clang diagnostic push
clang diagnostic ignored "-Wunknown-warning-option" clang diagnostic ignored "-Wunsafe-buffer-usage" clang diagnostic ignored "-Wunsafe-buffer-usage-in-libc-call" clang diagnostic ignored "-Wunsafe-buffer-usage-in-format-attr-call" [[clang ::suppress]] __extension__({ os_log_t _log_tmp = (LogEditing. osLogChannel); os_log_type_t _type_tmp = (OS_LOG_TYPE_DEFAULT ); if (os_log_type_enabled(_log_tmp, _type_tmp)) { __extension__ ({ clang diagnostic push clang diagnostic ignored "-Wvla" clang diagnostic error "-Wformat" _Static_assert(__builtin_constant_p ("Editor %p suppressed editing for Writing Tools"), "format/label/description argument must be a string constant" ); __attribute__((section("__TEXT,__oslogstring,cstring_literals" ),internal_linkage)) static const char _os_fmt_str[] __asm("LOS_LOG3" ) = "Editor %p suppressed editing for Writing Tools"; uint8_t _Alignas(16) __attribute__((uninitialized)) _os_fmt_buf[__builtin_os_log_format_buffer_size ("Editor %p suppressed editing for Writing Tools", this)]; _os_log_impl (&__dso_handle, _log_tmp, _type_tmp, _os_fmt_str, (uint8_t *)__builtin_os_log_format(_os_fmt_buf, "Editor %p suppressed editing for Writing Tools" , this), (uint32_t)sizeof(_os_fmt_buf)) clang diagnostic pop ; }); } }) clang diagnostic pop ; |
| 1227 | return false; |
| 1228 | } |
| 1229 | #endif |
| 1230 | |
| 1231 | m_hasHandledAnyEditing = true; |
| 1232 | |
| 1233 | if (!command.shouldDispatchInputEvents()) |
| 1234 | return true; |
| 1235 | |
| 1236 | RefPtr composition { command.composition() }; |
| 1237 | if (!composition) |
| 1238 | return true; |
| 1239 | |
| 1240 | if (command.isTopLevelCommand() && command.isTypingCommand() && document().view()) |
| 1241 | m_prohibitScrollingDueToContentSizeChangesWhileTyping = document().view()->prohibitScrollingWhenChangingContentSizeForScope(); |
| 1242 | |
| 1243 | auto isInputMethodComposing = command.isInputMethodComposing() ? IsInputMethodComposing::Yes : IsInputMethodComposing::No; |
| 1244 | return dispatchBeforeInputEvents(composition->startingRootEditableElement(), composition->endingRootEditableElement(), command.inputEventTypeName(), isInputMethodComposing, |
| 1245 | command.inputEventData(), command.inputEventDataTransfer(), targetRanges, command.isBeforeInputEventCancelable() ? Event::IsCancelable::Yes : Event::IsCancelable::No); |
| 1246 | } |
| 1247 | |
| 1248 | void Editor::appliedEditing(CompositeEditCommand& command) |
| 1249 | { |
| 1250 | LOG(Editing, "Editor %p appliedEditing", this)((void)0); |
| 1251 | |
| 1252 | Ref document = this->document(); |
| 1253 | document->updateLayout(); |
| 1254 | |
| 1255 | ASSERT(command.composition())((void)0); |
| 1256 | Ref composition = *command.composition(); |
| 1257 | VisibleSelection newSelection(command.endingSelection()); |
| 1258 | |
| 1259 | bool wasUserEdit = [&command] { |
| 1260 | RefPtr typingCommand = dynamicDowncast<TypingCommand>(command); |
| 1261 | return !typingCommand || !typingCommand->triggeringEventIsUntrusted(); |
| 1262 | }(); |
| 1263 | notifyTextFromControls(composition->startingRootEditableElement(), composition->endingRootEditableElement(), wasUserEdit); |
| 1264 | |
| 1265 | if (command.isTopLevelCommand()) { |
| 1266 | // Don't clear the typing style with this selection change. We do those things elsewhere if necessary. |
| 1267 | OptionSet<FrameSelection::SetSelectionOption> options; |
| 1268 | if (command.isDictationCommand()) |
| 1269 | options.add(FrameSelection::SetSelectionOption::DictationTriggered); |
| 1270 | |
| 1271 | changeSelectionAfterCommand(newSelection, options); |
| 1272 | } |
| 1273 | |
| 1274 | auto isInputMethodComposing = command.isInputMethodComposing() ? IsInputMethodComposing::Yes : IsInputMethodComposing::No; |
| 1275 | if (command.shouldDispatchInputEvents()) { |
| 1276 | dispatchInputEvents(composition->startingRootEditableElement(), composition->endingRootEditableElement(), command.inputEventTypeName(), isInputMethodComposing, |
| 1277 | command.inputEventData(), command.inputEventDataTransfer()); |
| 1278 | } |
| 1279 | |
| 1280 | if (command.isTopLevelCommand()) { |
| 1281 | updateEditorUINowIfScheduled(); |
| 1282 | |
| 1283 | m_alternativeTextController->respondToAppliedEditing(&command); |
| 1284 | |
| 1285 | if (!command.preservesTypingStyle()) |
| 1286 | document->selection().clearTypingStyle(); |
| 1287 | |
| 1288 | // Command will be equal to last edit command only in the case of typing |
| 1289 | if (m_lastEditCommand.get() == &command) |
| 1290 | ASSERT(command.isTypingCommand())((void)0); |
| 1291 | else { |
| 1292 | // Only register a new undo command if the command passed in is |
| 1293 | // different from the last command |
| 1294 | m_lastEditCommand = command; |
| 1295 | if (client()) |
| 1296 | client()->registerUndoStep(m_lastEditCommand->ensureComposition()); |
| 1297 | } |
| 1298 | respondToChangedContents(newSelection); |
| 1299 | |
| 1300 | if (command.isTypingCommand()) |
| 1301 | m_prohibitScrollingDueToContentSizeChangesWhileTyping = nullptr; |
| 1302 | } |
| 1303 | } |
| 1304 | |
| 1305 | bool Editor::willUnapplyEditing(const EditCommandComposition& composition) const |
| 1306 | { |
| 1307 | TypingCommand::closeTyping(protect(document())); |
| 1308 | return dispatchBeforeInputEvents(composition.startingRootEditableElement(), composition.endingRootEditableElement(), "historyUndo"_s, IsInputMethodComposing::No); |
| 1309 | } |
| 1310 | |
| 1311 | void Editor::unappliedEditing(EditCommandComposition& composition) |
| 1312 | { |
| 1313 | protect(document())->updateLayout(); |
| 1314 | |
| 1315 | notifyTextFromControls(composition.startingRootEditableElement(), composition.endingRootEditableElement()); |
| 1316 | |
| 1317 | VisibleSelection newSelection(composition.startingSelection()); |
| 1318 | changeSelectionAfterCommand(newSelection, FrameSelection::defaultSetSelectionOptions()); |
| 1319 | dispatchInputEvents(composition.startingRootEditableElement(), composition.endingRootEditableElement(), "historyUndo"_s, IsInputMethodComposing::No); |
| 1320 | |
| 1321 | updateEditorUINowIfScheduled(); |
| 1322 | |
| 1323 | m_alternativeTextController->respondToUnappliedEditing(&composition); |
| 1324 | #if ENABLE(WRITING_TOOLS)(defined 1 && 1) |
| 1325 | if (RefPtr page = document().page()) |
| 1326 | page->respondToUnappliedWritingToolsEditing(&composition); |
| 1327 | #endif |
| 1328 | |
| 1329 | m_lastEditCommand = nullptr; |
| 1330 | if (CheckedPtr client = this->client()) |
| 1331 | client->registerRedoStep(composition); |
| 1332 | respondToChangedContents(newSelection); |
| 1333 | } |
| 1334 | |
| 1335 | bool Editor::willReapplyEditing(const EditCommandComposition& composition) const |
| 1336 | { |
| 1337 | return dispatchBeforeInputEvents(composition.startingRootEditableElement(), composition.endingRootEditableElement(), "historyRedo"_s, IsInputMethodComposing::No); |
| 1338 | } |
| 1339 | |
| 1340 | void Editor::reappliedEditing(EditCommandComposition& composition) |
| 1341 | { |
| 1342 | protect(document())->updateLayout(); |
| 1343 | |
| 1344 | notifyTextFromControls(composition.startingRootEditableElement(), composition.endingRootEditableElement()); |
| 1345 | |
| 1346 | VisibleSelection newSelection(composition.endingSelection()); |
| 1347 | changeSelectionAfterCommand(newSelection, FrameSelection::defaultSetSelectionOptions()); |
| 1348 | dispatchInputEvents(composition.startingRootEditableElement(), composition.endingRootEditableElement(), "historyRedo"_s, IsInputMethodComposing::No); |
| 1349 | |
| 1350 | updateEditorUINowIfScheduled(); |
| 1351 | |
| 1352 | #if ENABLE(WRITING_TOOLS)(defined 1 && 1) |
| 1353 | if (RefPtr page = document().page()) |
| 1354 | page->respondToReappliedWritingToolsEditing(&composition); |
| 1355 | #endif |
| 1356 | |
| 1357 | m_lastEditCommand = nullptr; |
| 1358 | if (CheckedPtr client = this->client()) |
| 1359 | client->registerUndoStep(composition); |
| 1360 | respondToChangedContents(newSelection); |
| 1361 | } |
| 1362 | |
| 1363 | Editor::Editor(Document& document) |
| 1364 | : m_client(document.page() ? &document.page()->editorClient() : nullptr) |
| 1365 | , m_document(document) |
| 1366 | , m_killRing(makeUniqueRef<PAL::KillRing>()) |
| 1367 | , m_spellChecker(makeUniqueRefWithoutRefCountedCheck<SpellChecker>(*this)) |
| 1368 | , m_alternativeTextController(makeUniqueRef<AlternativeTextController>(document)) |
| 1369 | , m_editorUIUpdateTimer(*this, &Editor::editorUIUpdateTimerFired) |
| 1370 | #if ENABLE(TELEPHONE_NUMBER_DETECTION)(defined 1 && 1) && !PLATFORM(IOS_FAMILY)(defined 1 && 1) |
| 1371 | , m_telephoneNumberDetectionUpdateTimer(*this, &Editor::scanSelectionForTelephoneNumbers, 0_s) |
| 1372 | #endif |
| 1373 | { |
| 1374 | } |
| 1375 | |
| 1376 | Editor::~Editor() = default; |
| 1377 | |
| 1378 | void Editor::ref() const |
| 1379 | { |
| 1380 | m_document->ref(); |
| 1381 | } |
| 1382 | |
| 1383 | void Editor::deref() const |
| 1384 | { |
| 1385 | m_document->deref(); |
| 1386 | } |
| 1387 | |
| 1388 | void Editor::clear() |
| 1389 | { |
| 1390 | m_lastEditCommand = nullptr; |
| 1391 | if (m_compositionNode) { |
| 1392 | m_compositionNode = nullptr; |
| 1393 | if (CheckedPtr client = this->client()) |
| 1394 | client->discardedComposition(protect(document())); |
| 1395 | } |
| 1396 | m_customCompositionUnderlines.clear(); |
| 1397 | m_customCompositionHighlights.clear(); |
| 1398 | m_customCompositionAnnotations.clear(); |
| 1399 | m_shouldStyleWithCSS = false; |
| 1400 | m_defaultParagraphSeparator = EditorParagraphSeparator::div; |
| 1401 | m_mark = { }; |
| 1402 | m_oldSelectionForEditorUIUpdate = { }; |
| 1403 | m_editorUIUpdateTimer.stop(); |
| 1404 | m_alternativeTextController->stopAlternativeTextUITimer(); |
| 1405 | |
| 1406 | #if ENABLE(TELEPHONE_NUMBER_DETECTION)(defined 1 && 1) && !PLATFORM(IOS_FAMILY)(defined 1 && 1) |
| 1407 | m_telephoneNumberDetectionUpdateTimer.stop(); |
| 1408 | m_detectedTelephoneNumberRanges.clear(); |
| 1409 | #endif |
| 1410 | } |
| 1411 | |
| 1412 | bool Editor::insertText(const String& text, Event* triggeringEvent, TextEventInputType inputType) |
| 1413 | { |
| 1414 | return document().frame()->eventHandler().handleTextInputEvent(text, triggeringEvent, inputType); |
| 1415 | } |
| 1416 | |
| 1417 | bool Editor::insertTextForConfirmedComposition(const String& text) |
| 1418 | { |
| 1419 | return document().frame()->eventHandler().handleTextInputEvent(text, 0, TextEventInputComposition); |
| 1420 | } |
| 1421 | |
| 1422 | bool Editor::insertDictatedText(const String& text, const Vector<DictationAlternative>& dictationAlternatives, Event* triggeringEvent) |
| 1423 | { |
| 1424 | return m_alternativeTextController->insertDictatedText(text, dictationAlternatives, triggeringEvent); |
| 1425 | } |
| 1426 | |
| 1427 | bool Editor::insertTextWithoutSendingTextEvent(const String& text, bool selectInsertedText, TextEvent* triggeringEvent) |
| 1428 | { |
| 1429 | if (text.isEmpty()) |
| 1430 | return false; |
| 1431 | |
| 1432 | VisibleSelection selection = selectionForCommand(triggeringEvent); |
| 1433 | if (!selection.isContentEditable()) |
| 1434 | return false; |
| 1435 | |
| 1436 | if (!shouldInsertText(text, selection.toNormalizedRange(), EditorInsertAction::Typed)) |
| 1437 | return true; |
| 1438 | |
| 1439 | // FIXME: Should pass false to updateMarkersForWordsAffectedByEditing() to not remove markers if |
| 1440 | // a leading or trailing no-break space is being inserted. See <https://webkit.org/b/212098>. |
| 1441 | bool isStartOfNewWord = deprecatedIsSpaceOrNewline(selection.visibleStart().characterBefore()); |
| 1442 | updateMarkersForWordsAffectedByEditing(deprecatedIsSpaceOrNewline(text[0]) || isStartOfNewWord); |
| 1443 | |
| 1444 | bool shouldConsiderApplyingAutocorrection = false; |
| 1445 | if (text == " "_s || text == "\t"_s) |
| 1446 | shouldConsiderApplyingAutocorrection = true; |
| 1447 | |
| 1448 | if (text.length() == 1 && u_ispunct(text[0]) && !isAmbiguousBoundaryCharacter(text[0])) |
| 1449 | shouldConsiderApplyingAutocorrection = true; |
| 1450 | |
| 1451 | bool autocorrectionWasApplied = shouldConsiderApplyingAutocorrection && didApplyAutocorrection(document(), m_alternativeTextController); |
| 1452 | |
| 1453 | // Get the selection to use for the event that triggered this insertText. |
| 1454 | // If the event handler changed the selection, we may want to use a different selection |
| 1455 | // that is contained in the event target. |
| 1456 | selection = selectionForCommand(triggeringEvent); |
| 1457 | if (selection.isContentEditable()) { |
| 1458 | if (RefPtr selectionStart = selection.start().deprecatedNode()) { |
| 1459 | Ref document = selectionStart->document(); |
| 1460 | |
| 1461 | // Insert the text |
| 1462 | if (triggeringEvent && triggeringEvent->isDictation()) |
| 1463 | DictationCommand::insertText(document.copyRef(), text, triggeringEvent->dictationAlternatives(), selection); |
| 1464 | else { |
| 1465 | auto options = OptionSet { TypingCommand::Option::RetainAutocorrectionIndicator }; |
| 1466 | if (selectInsertedText) |
| 1467 | options.add(TypingCommand::Option::SelectInsertedText); |
| 1468 | if (triggeringEvent && triggeringEvent->isAutocompletion()) |
| 1469 | options.add(TypingCommand::Option::IsAutocompletion); |
| 1470 | if (shouldRemoveAutocorrectionIndicator(shouldConsiderApplyingAutocorrection, autocorrectionWasApplied, options.contains(TypingCommand::Option::IsAutocompletion))) |
| 1471 | options.remove(TypingCommand::Option::RetainAutocorrectionIndicator); |
| 1472 | auto compositionType = triggeringEvent && triggeringEvent->isComposition() ? TypingCommand::TextCompositionType::Final : TypingCommand::TextCompositionType::None; |
| 1473 | TypingCommand::insertText(document.copyRef(), text, triggeringEvent, selection, options, compositionType); |
| 1474 | } |
| 1475 | |
| 1476 | // Reveal the current selection. Note that focus may have changed after insertion. |
| 1477 | // FIXME: Selection is allowed even if setIgnoreSelectionChanges(true). Ideally setIgnoreSelectionChanges() |
| 1478 | // should be moved from Editor to a page-level like object. If it must remain a frame-specific concept |
| 1479 | // then this code should conditionalize revealing selection on whether the ignoreSelectionChanges() bit |
| 1480 | // is set for the newly focused frame. |
| 1481 | if ((!triggeringEvent || triggeringEvent->isTrusted()) && client() && client()->shouldRevealCurrentSelectionAfterInsertion()) { |
| 1482 | if (RefPtr page = document->page()) |
| 1483 | page->revealCurrentSelection(); |
| 1484 | } |
| 1485 | } |
| 1486 | } |
| 1487 | |
| 1488 | return true; |
| 1489 | } |
| 1490 | |
| 1491 | bool Editor::insertLineBreak() |
| 1492 | { |
| 1493 | if (!canEdit()) |
| 1494 | return false; |
| 1495 | |
| 1496 | Ref document = this->document(); |
| 1497 | if (!shouldInsertText("\n"_s, document->selection().selection().toNormalizedRange(), EditorInsertAction::Typed)) |
| 1498 | return true; |
| 1499 | |
| 1500 | VisiblePosition caret = document->selection().selection().visibleStart(); |
| 1501 | bool alignToEdge = isEndOfEditableOrNonEditableContent(caret); |
| 1502 | bool autocorrectionIsApplied = m_alternativeTextController->applyAutocorrectionBeforeTypingIfAppropriate(); |
| 1503 | TypingCommand::insertLineBreak(WTF::move(document), autocorrectionIsApplied ? TypingCommand::Option::RetainAutocorrectionIndicator : OptionSet<TypingCommand::Option> { }); |
| 1504 | revealSelectionAfterEditingOperation(alignToEdge ? ScrollAlignment::alignToEdgeIfNeeded : ScrollAlignment::alignCenterIfNeeded); |
| 1505 | |
| 1506 | return true; |
| 1507 | } |
| 1508 | |
| 1509 | bool Editor::insertParagraphSeparator() |
| 1510 | { |
| 1511 | if (!canEdit()) |
| 1512 | return false; |
| 1513 | |
| 1514 | if (!canEditRichly()) |
| 1515 | return insertLineBreak(); |
| 1516 | |
| 1517 | Ref document = this->document(); |
| 1518 | if (!shouldInsertText("\n"_s, document->selection().selection().toNormalizedRange(), EditorInsertAction::Typed)) |
| 1519 | return true; |
| 1520 | |
| 1521 | VisiblePosition caret = document->selection().selection().visibleStart(); |
| 1522 | bool alignToEdge = isEndOfEditableOrNonEditableContent(caret); |
| 1523 | bool autocorrectionIsApplied = m_alternativeTextController->applyAutocorrectionBeforeTypingIfAppropriate(); |
| 1524 | TypingCommand::insertParagraphSeparator(WTF::move(document), autocorrectionIsApplied ? TypingCommand::Option::RetainAutocorrectionIndicator : OptionSet<TypingCommand::Option> { }); |
| 1525 | revealSelectionAfterEditingOperation(alignToEdge ? ScrollAlignment::alignToEdgeIfNeeded : ScrollAlignment::alignCenterIfNeeded); |
| 1526 | |
| 1527 | return true; |
| 1528 | } |
| 1529 | |
| 1530 | bool Editor::insertParagraphSeparatorInQuotedContent() |
| 1531 | { |
| 1532 | // FIXME: Why is this missing calls to canEdit, canEditRichly, etc.? |
| 1533 | TypingCommand::insertParagraphSeparatorInQuotedContent(protect(document())); |
| 1534 | revealSelectionAfterEditingOperation(); |
| 1535 | return true; |
| 1536 | } |
| 1537 | |
| 1538 | void Editor::cut(FromMenuOrKeyBinding fromMenuOrKeyBinding) |
| 1539 | { |
| 1540 | SetForScope copyScope { m_copyingFromMenuOrKeyBinding, fromMenuOrKeyBinding == FromMenuOrKeyBinding::Yes }; |
| 1541 | if (tryDHTMLCut()) |
| 1542 | return; // DHTML did the whole operation |
| 1543 | if (!canCut()) { |
| 1544 | SystemSoundManager::singleton().systemBeep(); |
| 1545 | return; |
| 1546 | } |
| 1547 | |
| 1548 | performCutOrCopy(CutAction); |
| 1549 | } |
| 1550 | |
| 1551 | void Editor::copy(FromMenuOrKeyBinding fromMenuOrKeyBinding) |
| 1552 | { |
| 1553 | SetForScope copyScope { m_copyingFromMenuOrKeyBinding, fromMenuOrKeyBinding == FromMenuOrKeyBinding::Yes }; |
| 1554 | if (tryDHTMLCopy()) |
| 1555 | return; // DHTML did the whole operation |
| 1556 | if (!canCopy()) { |
| 1557 | SystemSoundManager::singleton().systemBeep(); |
| 1558 | return; |
| 1559 | } |
| 1560 | |
| 1561 | performCutOrCopy(CopyAction); |
| 1562 | } |
| 1563 | |
| 1564 | void Editor::copyFont(FromMenuOrKeyBinding fromMenuOrKeyBinding) |
| 1565 | { |
| 1566 | SetForScope copyScope { m_copyingFromMenuOrKeyBinding, fromMenuOrKeyBinding == FromMenuOrKeyBinding::Yes }; |
| 1567 | if (tryDHTMLCopy()) |
| 1568 | return; // DHTML did the whole operation |
| 1569 | if (!canCopyFont()) { |
| 1570 | SystemSoundManager::singleton().systemBeep(); |
| 1571 | return; |
| 1572 | } |
| 1573 | |
| 1574 | willWriteSelectionToPasteboard(selectedRange()); |
| 1575 | platformCopyFont(); |
| 1576 | didWriteSelectionToPasteboard(); |
| 1577 | } |
| 1578 | |
| 1579 | bool Editor::canCopyFont() const |
| 1580 | { |
| 1581 | auto& selection = document().selection().selection(); |
| 1582 | if (selection.isNone() || (selection.isInPasswordField() && !selection.isInAutoFilledAndViewableField())) |
| 1583 | return false; |
| 1584 | return selection.isRange() || selection.isContentEditable(); |
| 1585 | } |
| 1586 | |
| 1587 | void Editor::postTextStateChangeNotificationForCut(const String& text, const VisibleSelection& selection) |
| 1588 | { |
| 1589 | if (!AXObjectCache::accessibilityEnabled()) |
| 1590 | return; |
| 1591 | if (!text.length()) |
| 1592 | return; |
| 1593 | CheckedPtr cache = document().existingAXObjectCache(); |
| 1594 | if (!cache) |
| 1595 | return; |
| 1596 | cache->postTextStateChangeNotification(selection.start().anchorNode(), AXTextEditType::Cut, text, selection.start()); |
| 1597 | } |
| 1598 | |
| 1599 | void Editor::performCutOrCopy(EditorActionSpecifier action) |
| 1600 | { |
| 1601 | auto selection = selectedRange(); |
| 1602 | willWriteSelectionToPasteboard(selection); |
| 1603 | if (action == CutAction) { |
| 1604 | if (!shouldDeleteRange(selection)) |
| 1605 | return; |
| 1606 | |
| 1607 | updateMarkersForWordsAffectedByEditing(true); |
| 1608 | } |
| 1609 | |
| 1610 | Ref document = this->document(); |
| 1611 | if (enclosingTextFormControl(document->selection().selection().start())) |
| 1612 | Pasteboard::createForCopyAndPaste(PagePasteboardContext::create(document->pageID()))->writePlainText(selectedTextForDataTransfer(), canSmartCopyOrDelete() ? Pasteboard::CanSmartReplace : Pasteboard::CannotSmartReplace); |
| 1613 | else { |
| 1614 | RefPtr<HTMLImageElement> imageElement; |
| 1615 | bool isSelectionInImageOverlay = selection && !selection->collapsed() && ImageOverlay::isInsideOverlay(*selection); |
| 1616 | if (action == CopyAction && !isSelectionInImageOverlay) |
| 1617 | imageElement = imageElementFromImageDocument(document); |
| 1618 | |
| 1619 | if (imageElement) { |
| 1620 | #if !PLATFORM(WIN)(defined WTF_PLATFORM_WIN && WTF_PLATFORM_WIN) |
| 1621 | writeImageToPasteboard(*Pasteboard::createForCopyAndPaste(PagePasteboardContext::create(document->pageID())), *imageElement, document->url(), document->title()); |
| 1622 | #else |
| 1623 | // FIXME: Delete after <http://webkit.org/b/177618> lands. |
| 1624 | Pasteboard::createForCopyAndPaste(PagePasteboardContext::create(document->pageID()))->writeImage(*imageElement, document->url(), document->title()); |
| 1625 | #endif |
| 1626 | } else { |
| 1627 | #if !PLATFORM(WIN)(defined WTF_PLATFORM_WIN && WTF_PLATFORM_WIN) |
| 1628 | writeSelectionToPasteboard(*Pasteboard::createForCopyAndPaste(PagePasteboardContext::create(document->pageID()))); |
| 1629 | #else |
| 1630 | // FIXME: Delete after <http://webkit.org/b/177618> lands. |
| 1631 | Pasteboard::createForCopyAndPaste(PagePasteboardContext::create(document->pageID()))->writeSelection(selection, canSmartCopyOrDelete(), *document->frame(), IncludeImageAltTextForDataTransfer); |
| 1632 | #endif |
| 1633 | } |
| 1634 | } |
| 1635 | |
| 1636 | didWriteSelectionToPasteboard(); |
| 1637 | if (action == CutAction) { |
| 1638 | String text; |
| 1639 | if (AXObjectCache::accessibilityEnabled()) |
| 1640 | text = AccessibilityObject::stringForVisiblePositionRange(document->selection().selection()); |
| 1641 | deleteSelectionWithSmartDelete(canSmartCopyOrDelete(), EditAction::Cut); |
| 1642 | if (AXObjectCache::accessibilityEnabled()) |
| 1643 | postTextStateChangeNotificationForCut(text, document->selection().selection()); |
| 1644 | } |
| 1645 | } |
| 1646 | |
| 1647 | void Editor::paste(FromMenuOrKeyBinding fromMenuOrKeyBinding) |
| 1648 | { |
| 1649 | paste(*Pasteboard::createForCopyAndPaste(PagePasteboardContext::create(document().pageID())), fromMenuOrKeyBinding); |
| 1650 | } |
| 1651 | |
| 1652 | void Editor::paste(Pasteboard& pasteboard, FromMenuOrKeyBinding fromMenuOrKeyBinding) |
| 1653 | { |
| 1654 | SetForScope pasteScope { m_pastingFromMenuOrKeyBinding, fromMenuOrKeyBinding == FromMenuOrKeyBinding::Yes }; |
| 1655 | |
| 1656 | if (!dispatchClipboardEvent(findEventTargetFromSelection(), ClipboardEventKind::Paste)) |
| 1657 | return; // DHTML did the whole operation |
| 1658 | if (!canEdit()) |
| 1659 | return; |
| 1660 | updateMarkersForWordsAffectedByEditing(false); |
| 1661 | ResourceCacheValidationSuppressor validationSuppressor(document().cachedResourceLoader()); |
| 1662 | if (document().selection().selection().isContentRichlyEditable()) |
| 1663 | pasteWithPasteboard(&pasteboard, { PasteOption::AllowPlainText }); |
| 1664 | else |
| 1665 | pasteAsPlainTextWithPasteboard(pasteboard); |
| 1666 | } |
| 1667 | |
| 1668 | void Editor::pasteAsPlainText(FromMenuOrKeyBinding fromMenuOrKeyBinding) |
| 1669 | { |
| 1670 | SetForScope pasteScope { m_pastingFromMenuOrKeyBinding, fromMenuOrKeyBinding == FromMenuOrKeyBinding::Yes }; |
| 1671 | |
| 1672 | if (!dispatchClipboardEvent(findEventTargetFromSelection(), ClipboardEventKind::PasteAsPlainText)) |
| 1673 | return; |
| 1674 | if (!canEdit()) |
| 1675 | return; |
| 1676 | updateMarkersForWordsAffectedByEditing(false); |
| 1677 | pasteAsPlainTextWithPasteboard(*Pasteboard::createForCopyAndPaste(PagePasteboardContext::create(document().pageID()))); |
| 1678 | } |
| 1679 | |
| 1680 | void Editor::pasteAsQuotation(FromMenuOrKeyBinding fromMenuOrKeyBinding) |
| 1681 | { |
| 1682 | SetForScope pasteScope { m_pastingFromMenuOrKeyBinding, fromMenuOrKeyBinding == FromMenuOrKeyBinding::Yes }; |
| 1683 | |
| 1684 | if (!dispatchClipboardEvent(findEventTargetFromSelection(), ClipboardEventKind::PasteAsQuotation)) |
| 1685 | return; |
| 1686 | if (!canEdit()) |
| 1687 | return; |
| 1688 | updateMarkersForWordsAffectedByEditing(false); |
| 1689 | Ref document = this->document(); |
| 1690 | ResourceCacheValidationSuppressor validationSuppressor(document->cachedResourceLoader()); |
| 1691 | auto pasteboard = Pasteboard::createForCopyAndPaste(PagePasteboardContext::create(document->pageID())); |
| 1692 | if (document->selection().selection().isContentRichlyEditable()) |
| 1693 | pasteWithPasteboard(pasteboard.get(), { PasteOption::AllowPlainText, PasteOption::AsQuotation }); |
| 1694 | else |
| 1695 | pasteAsPlainTextWithPasteboard(*pasteboard); |
| 1696 | } |
| 1697 | |
| 1698 | void Editor::pasteFont(FromMenuOrKeyBinding fromMenuOrKeyBinding) |
| 1699 | { |
| 1700 | SetForScope pasteScope { m_pastingFromMenuOrKeyBinding, fromMenuOrKeyBinding == FromMenuOrKeyBinding::Yes }; |
| 1701 | |
| 1702 | if (!dispatchClipboardEvent(findEventTargetFromSelection(), ClipboardEventKind::PasteFont)) |
| 1703 | return; |
| 1704 | if (!canEdit()) |
| 1705 | return; |
| 1706 | updateMarkersForWordsAffectedByEditing(false); |
| 1707 | ResourceCacheValidationSuppressor validationSuppressor(document().cachedResourceLoader()); |
| 1708 | platformPasteFont(); |
| 1709 | } |
| 1710 | |
| 1711 | void Editor::quoteFragmentForPasting(DocumentFragment& fragment) |
| 1712 | { |
| 1713 | Ref document = this->document(); |
| 1714 | auto blockQuote = HTMLQuoteElement::create(blockquoteTag, document); |
| 1715 | blockQuote->setAttributeWithoutSynchronization(typeAttr, "cite"_s); |
| 1716 | blockQuote->setAttributeWithoutSynchronization(classAttr, ApplePasteAsQuotation); |
| 1717 | |
| 1718 | RefPtr childNode = fragment.firstChild(); |
| 1719 | |
| 1720 | if (childNode) { |
| 1721 | while (childNode) { |
| 1722 | blockQuote->appendChild(*childNode); |
| 1723 | childNode = fragment.firstChild(); |
| 1724 | } |
| 1725 | } else |
| 1726 | blockQuote->appendChild(HTMLBRElement::create(document)); |
| 1727 | |
| 1728 | fragment.appendChild(blockQuote); |
| 1729 | } |
| 1730 | |
| 1731 | void Editor::performDelete() |
| 1732 | { |
| 1733 | if (!canDelete()) { |
| 1734 | SystemSoundManager::singleton().systemBeep(); |
| 1735 | return; |
| 1736 | } |
| 1737 | |
| 1738 | addRangeToKillRing(*selectedRange(), KillRingInsertionMode::AppendText); |
| 1739 | deleteSelectionWithSmartDelete(canSmartCopyOrDelete()); |
| 1740 | |
| 1741 | // clear the "start new kill ring sequence" setting, because it was set to true |
| 1742 | // when the selection was updated by deleting the range |
| 1743 | setStartNewKillRingSequence(false); |
| 1744 | } |
| 1745 | |
| 1746 | void Editor::changeSelectionListType() |
| 1747 | { |
| 1748 | Ref document = this->document(); |
| 1749 | if (auto type = ChangeListTypeCommand::listConversionType(document)) |
| 1750 | ChangeListTypeCommand::create(WTF::move(document), *type)->apply(); |
| 1751 | } |
| 1752 | |
| 1753 | void Editor::simplifyMarkup(Node* startNode, Node* endNode) |
| 1754 | { |
| 1755 | if (!startNode) |
| 1756 | return; |
| 1757 | |
| 1758 | RefPtr<Node> pastEndNode; |
| 1759 | if (endNode) { |
| 1760 | if (!is_lt(treeOrder(*startNode, *endNode))) |
| 1761 | return; |
| 1762 | pastEndNode = NodeTraversal::next(*endNode); |
| 1763 | } |
| 1764 | |
| 1765 | SimplifyMarkupCommand::create(protect(document()), startNode, pastEndNode.get())->apply(); |
| 1766 | } |
| 1767 | |
| 1768 | void Editor::copyURL(const URL& url, const String& title) |
| 1769 | { |
| 1770 | copyURL(url, title, *Pasteboard::createForCopyAndPaste(PagePasteboardContext::create(document().pageID()))); |
| 1771 | } |
| 1772 | |
| 1773 | void Editor::copyURL(const URL& url, const String& title, Pasteboard& pasteboard) |
| 1774 | { |
| 1775 | auto sanitizedURL = url; |
| 1776 | if (RefPtr page = document().page()) |
| 1777 | sanitizedURL = page->applyLinkDecorationFiltering(url, LinkDecorationFilteringTrigger::Copy); |
| 1778 | |
| 1779 | PasteboardURL pasteboardURL; |
| 1780 | pasteboardURL.url = sanitizedURL; |
| 1781 | pasteboardURL.title = title; |
| 1782 | |
| 1783 | #if PLATFORM(MAC)(defined WTF_PLATFORM_MAC && WTF_PLATFORM_MAC) |
| 1784 | pasteboardURL.userVisibleForm = userVisibleString(sanitizedURL); |
| 1785 | #endif |
| 1786 | |
| 1787 | pasteboard.write(pasteboardURL); |
| 1788 | } |
| 1789 | |
| 1790 | PasteboardWriterData::URLData Editor::pasteboardWriterURL(const URL& url, const String& title) |
| 1791 | { |
| 1792 | PasteboardWriterData::URLData result; |
| 1793 | |
| 1794 | result.url = url; |
| 1795 | result.title = title; |
| 1796 | #if PLATFORM(MAC)(defined WTF_PLATFORM_MAC && WTF_PLATFORM_MAC) |
| 1797 | result.userVisibleForm = userVisibleString(url); |
| 1798 | #endif |
| 1799 | |
| 1800 | return result; |
| 1801 | } |
| 1802 | |
| 1803 | #if !PLATFORM(IOS_FAMILY)(defined 1 && 1) |
| 1804 | |
| 1805 | void Editor::copyImage(const HitTestResult& result) |
| 1806 | { |
| 1807 | RefPtr element { result.innerNonSharedElement() }; |
| 1808 | if (!element) |
| 1809 | return; |
| 1810 | |
| 1811 | URL url = result.absoluteLinkURL(); |
| 1812 | if (url.isEmpty()) |
| 1813 | url = result.absoluteImageURL(); |
| 1814 | |
| 1815 | #if !PLATFORM(WIN)(defined WTF_PLATFORM_WIN && WTF_PLATFORM_WIN) |
| 1816 | writeImageToPasteboard(*Pasteboard::createForCopyAndPaste(PagePasteboardContext::create(document().pageID())), *element, url, result.altDisplayString()); |
| 1817 | #else |
| 1818 | // FIXME: Delete after <http://webkit.org/b/177618> lands. |
| 1819 | Pasteboard::createForCopyAndPaste(PagePasteboardContext::create(document().pageID()))->writeImage(*element, url, result.altDisplayString()); |
| 1820 | #endif |
| 1821 | } |
| 1822 | |
| 1823 | #endif |
| 1824 | |
| 1825 | void Editor::revealSelectionIfNeededAfterLoadingImageForElement(HTMLImageElement& element) |
| 1826 | { |
| 1827 | if (m_imageElementsToLoadBeforeRevealingSelection.isEmpty()) |
| 1828 | return; |
| 1829 | |
| 1830 | if (!m_imageElementsToLoadBeforeRevealingSelection.remove(&element)) |
| 1831 | return; |
| 1832 | |
| 1833 | if (!m_imageElementsToLoadBeforeRevealingSelection.isEmpty()) |
| 1834 | return; |
| 1835 | |
| 1836 | // FIXME: This should be queued as a task for the next rendering update. |
| 1837 | Ref document = this->document(); |
| 1838 | document->updateLayout(); |
| 1839 | document->selection().setCaretRectNeedsUpdate(); |
| 1840 | revealSelectionAfterEditingOperation(); |
| 1841 | } |
| 1842 | |
| 1843 | void Editor::renderLayerDidScroll(const RenderLayer& layer) |
| 1844 | { |
| 1845 | if (m_imageElementsToLoadBeforeRevealingSelection.isEmpty()) |
| 1846 | return; |
| 1847 | |
| 1848 | RefPtr startContainer { document().selection().selection().start().containerNode() }; |
| 1849 | if (!startContainer) |
| 1850 | return; |
| 1851 | |
| 1852 | CheckedPtr startContainerRenderer = startContainer->renderer(); |
| 1853 | if (!startContainerRenderer) |
| 1854 | return; |
| 1855 | |
| 1856 | // FIXME: Ideally, this would also cancel deferred selection revealing if the selection is inside a subframe and a parent frame is scrolled. |
| 1857 | for (CheckedPtr enclosingLayer = startContainerRenderer->enclosingLayer(); enclosingLayer; enclosingLayer = enclosingLayer->parent()) { |
| 1858 | if (enclosingLayer.get() == &layer) { |
| 1859 | m_imageElementsToLoadBeforeRevealingSelection.clear(); |
| 1860 | break; |
| 1861 | } |
| 1862 | } |
| 1863 | } |
| 1864 | |
| 1865 | bool Editor::isContinuousSpellCheckingEnabled() const |
| 1866 | { |
| 1867 | return client() && client()->isContinuousSpellCheckingEnabled(); |
| 1868 | } |
| 1869 | |
| 1870 | void Editor::toggleContinuousSpellChecking() |
| 1871 | { |
| 1872 | if (client()) |
| 1873 | client()->toggleContinuousSpellChecking(); |
| 1874 | } |
| 1875 | |
| 1876 | bool Editor::isGrammarCheckingEnabled() |
| 1877 | { |
| 1878 | return client() && client()->isGrammarCheckingEnabled(); |
| 1879 | } |
| 1880 | |
| 1881 | void Editor::toggleGrammarChecking() |
| 1882 | { |
| 1883 | if (client()) |
| 1884 | client()->toggleGrammarChecking(); |
| 1885 | } |
| 1886 | |
| 1887 | int Editor::spellCheckerDocumentTag() |
| 1888 | { |
| 1889 | return client() ? client()->spellCheckerDocumentTag() : 0; |
| 1890 | } |
| 1891 | |
| 1892 | #if USE(APPKIT)(defined USE_APPKIT && USE_APPKIT) |
| 1893 | |
| 1894 | void Editor::uppercaseWord() |
| 1895 | { |
| 1896 | if (client()) |
| 1897 | client()->uppercaseWord(); |
| 1898 | } |
| 1899 | |
| 1900 | void Editor::lowercaseWord() |
| 1901 | { |
| 1902 | if (client()) |
| 1903 | client()->lowercaseWord(); |
| 1904 | } |
| 1905 | |
| 1906 | void Editor::capitalizeWord() |
| 1907 | { |
| 1908 | if (client()) |
| 1909 | client()->capitalizeWord(); |
| 1910 | } |
| 1911 | |
| 1912 | bool Editor::canApplyCaseTransformations(const String& selection) |
| 1913 | { |
| 1914 | if (client()) |
| 1915 | return client()->canApplyCaseTransformations(selection); |
| 1916 | |
| 1917 | return true; |
| 1918 | } |
| 1919 | |
| 1920 | bool Editor::canConvertToSimplifiedChinese(const String& selection) |
| 1921 | { |
| 1922 | if (client()) |
| 1923 | return client()->canConvertToSimplifiedChinese(selection); |
| 1924 | |
| 1925 | return false; |
| 1926 | } |
| 1927 | |
| 1928 | bool Editor::canConvertToTraditionalChinese(const String& selection) |
| 1929 | { |
| 1930 | if (client()) |
| 1931 | return client()->canConvertToTraditionalChinese(selection); |
| 1932 | |
| 1933 | return false; |
| 1934 | } |
| 1935 | |
| 1936 | void Editor::convertToTraditionalChinese() |
| 1937 | { |
| 1938 | if (client()) |
| 1939 | client()->convertToTraditionalChinese(); |
| 1940 | } |
| 1941 | |
| 1942 | void Editor::convertToSimplifiedChinese() |
| 1943 | { |
| 1944 | if (client()) |
| 1945 | client()->convertToSimplifiedChinese(); |
| 1946 | } |
| 1947 | |
| 1948 | #endif |
| 1949 | |
| 1950 | #if USE(AUTOMATIC_TEXT_REPLACEMENT)(defined USE_AUTOMATIC_TEXT_REPLACEMENT && USE_AUTOMATIC_TEXT_REPLACEMENT ) |
| 1951 | |
| 1952 | void Editor::showSubstitutionsPanel() |
| 1953 | { |
| 1954 | if (!client()) { |
| 1955 | LOG_ERROR("No NSSpellChecker")((void)0); |
| 1956 | return; |
| 1957 | } |
| 1958 | |
| 1959 | if (client()->substitutionsPanelIsShowing()) { |
| 1960 | client()->showSubstitutionsPanel(false); |
| 1961 | return; |
| 1962 | } |
| 1963 | client()->showSubstitutionsPanel(true); |
| 1964 | } |
| 1965 | |
| 1966 | bool Editor::substitutionsPanelIsShowing() |
| 1967 | { |
| 1968 | if (!client()) |
| 1969 | return false; |
| 1970 | return client()->substitutionsPanelIsShowing(); |
| 1971 | } |
| 1972 | |
| 1973 | void Editor::toggleSmartInsertDelete() |
| 1974 | { |
| 1975 | if (client()) |
| 1976 | client()->toggleSmartInsertDelete(); |
| 1977 | } |
| 1978 | |
| 1979 | bool Editor::isAutomaticQuoteSubstitutionEnabled() |
| 1980 | { |
| 1981 | return client() && client()->isAutomaticQuoteSubstitutionEnabled(); |
| 1982 | } |
| 1983 | |
| 1984 | void Editor::toggleAutomaticQuoteSubstitution() |
| 1985 | { |
| 1986 | if (client()) |
| 1987 | client()->toggleAutomaticQuoteSubstitution(); |
| 1988 | } |
| 1989 | |
| 1990 | bool Editor::isAutomaticLinkDetectionEnabled() |
| 1991 | { |
| 1992 | return client() && client()->isAutomaticLinkDetectionEnabled(); |
| 1993 | } |
| 1994 | |
| 1995 | void Editor::toggleAutomaticLinkDetection() |
| 1996 | { |
| 1997 | if (client()) |
| 1998 | client()->toggleAutomaticLinkDetection(); |
| 1999 | } |
| 2000 | |
| 2001 | bool Editor::isAutomaticDashSubstitutionEnabled() |
| 2002 | { |
| 2003 | return client() && client()->isAutomaticDashSubstitutionEnabled(); |
| 2004 | } |
| 2005 | |
| 2006 | void Editor::toggleAutomaticDashSubstitution() |
| 2007 | { |
| 2008 | if (client()) |
| 2009 | client()->toggleAutomaticDashSubstitution(); |
| 2010 | } |
| 2011 | |
| 2012 | bool Editor::isAutomaticTextReplacementEnabled() |
| 2013 | { |
| 2014 | return client() && client()->isAutomaticTextReplacementEnabled(); |
| 2015 | } |
| 2016 | |
| 2017 | void Editor::toggleAutomaticTextReplacement() |
| 2018 | { |
| 2019 | if (client()) |
| 2020 | client()->toggleAutomaticTextReplacement(); |
| 2021 | } |
| 2022 | |
| 2023 | bool Editor::canEnableAutomaticSpellingCorrection() const |
| 2024 | { |
| 2025 | return m_alternativeTextController->canEnableAutomaticSpellingCorrection(); |
| 2026 | } |
| 2027 | |
| 2028 | bool Editor::isAutomaticSpellingCorrectionEnabled() |
| 2029 | { |
| 2030 | return m_alternativeTextController->isAutomaticSpellingCorrectionEnabled(); |
| 2031 | } |
| 2032 | |
| 2033 | void Editor::toggleAutomaticSpellingCorrection() |
| 2034 | { |
| 2035 | if (client()) |
| 2036 | client()->toggleAutomaticSpellingCorrection(); |
| 2037 | } |
| 2038 | |
| 2039 | void Editor::toggleSmartLists() |
| 2040 | { |
| 2041 | Ref document = this->document(); |
| 2042 | if (!document->settings().smartListsAvailable()) |
| 2043 | return; |
| 2044 | |
| 2045 | if (client()) |
| 2046 | client()->toggleSmartLists(); |
| 2047 | } |
| 2048 | |
| 2049 | #endif // USE(AUTOMATIC_TEXT_REPLACEMENT) |
| 2050 | |
| 2051 | #if PLATFORM(COCOA)(defined 1 && 1) |
| 2052 | bool Editor::isSmartListsEnabled() |
| 2053 | { |
| 2054 | Ref document = this->document(); |
| 2055 | if (!document->settings().smartListsAvailable()) |
| 2056 | return false; |
| 2057 | |
| 2058 | #if PLATFORM(MAC)(defined WTF_PLATFORM_MAC && WTF_PLATFORM_MAC) |
| 2059 | return client() && client()->isSmartListsEnabled(); |
| 2060 | #else |
| 2061 | return true; |
| 2062 | #endif |
| 2063 | } |
| 2064 | #endif // PLATFORM(COCOA) |
| 2065 | |
| 2066 | bool Editor::shouldEndEditing(const SimpleRange& range) |
| 2067 | { |
| 2068 | return client() && client()->shouldEndEditing(range); |
| 2069 | } |
| 2070 | |
| 2071 | bool Editor::shouldBeginEditing(const SimpleRange& range) |
| 2072 | { |
| 2073 | return client() && client()->shouldBeginEditing(range); |
| 2074 | } |
| 2075 | |
| 2076 | void Editor::clearUndoRedoOperations() |
| 2077 | { |
| 2078 | if (client()) |
| 2079 | client()->clearUndoRedoOperations(); |
| 2080 | } |
| 2081 | |
| 2082 | bool Editor::canUndo() const |
| 2083 | { |
| 2084 | return client() && client()->canUndo(); |
| 2085 | } |
| 2086 | |
| 2087 | void Editor::undo() |
| 2088 | { |
| 2089 | if (client()) |
| 2090 | client()->undo(); |
| 2091 | } |
| 2092 | |
| 2093 | bool Editor::canRedo() const |
| 2094 | { |
| 2095 | return client() && client()->canRedo(); |
| 2096 | } |
| 2097 | |
| 2098 | void Editor::redo() |
| 2099 | { |
| 2100 | if (client()) |
| 2101 | client()->redo(); |
| 2102 | } |
| 2103 | |
| 2104 | void Editor::registerCustomUndoStep(Ref<CustomUndoStep>&& undoStep) |
| 2105 | { |
| 2106 | ASSERT(document().settings().undoManagerAPIEnabled())((void)0); |
| 2107 | if (CheckedPtr client = this->client()) |
| 2108 | client->registerUndoStep(WTF::move(undoStep)); |
| 2109 | } |
| 2110 | |
| 2111 | void Editor::didBeginEditing() |
| 2112 | { |
| 2113 | if (client()) |
| 2114 | client()->didBeginEditing(); |
| 2115 | } |
| 2116 | |
| 2117 | void Editor::didEndEditing() |
| 2118 | { |
| 2119 | if (client()) |
| 2120 | client()->didEndEditing(); |
| 2121 | } |
| 2122 | |
| 2123 | void Editor::willWriteSelectionToPasteboard(const std::optional<SimpleRange>& range) |
| 2124 | { |
| 2125 | if (client()) |
| 2126 | client()->willWriteSelectionToPasteboard(range); |
| 2127 | } |
| 2128 | |
| 2129 | void Editor::didWriteSelectionToPasteboard() |
| 2130 | { |
| 2131 | if (client()) |
| 2132 | client()->didWriteSelectionToPasteboard(); |
| 2133 | } |
| 2134 | |
| 2135 | void Editor::toggleBold() |
| 2136 | { |
| 2137 | command("ToggleBold"_s).execute(); |
| 2138 | } |
| 2139 | |
| 2140 | void Editor::toggleUnderline() |
| 2141 | { |
| 2142 | command("ToggleUnderline"_s).execute(); |
| 2143 | } |
| 2144 | |
| 2145 | void Editor::setTextAlignmentForChangedBaseWritingDirection(WritingDirection direction) |
| 2146 | { |
| 2147 | // Note that the passed-in argument is the direction that has been changed to by |
| 2148 | // some code or user interaction outside the scope of this function. The former |
| 2149 | // direction is not known, nor is it required for the kind of text alignment |
| 2150 | // changes done by this function. |
| 2151 | // |
| 2152 | // Rules: |
| 2153 | // When text has no explicit alignment, set alignment to match the writing direction. |
| 2154 | // If the text has left or right alignment, flip left->right and right->left. |
| 2155 | // Otherwise, do nothing. |
| 2156 | |
| 2157 | Ref document = this->document(); |
| 2158 | RefPtr selectionStyle = EditingStyle::styleAtSelectionStart(document->selection().selection()); |
| 2159 | if (!selectionStyle || !selectionStyle->style()) |
| 2160 | return; |
| 2161 | |
| 2162 | auto value = selectionStyle->style()->propertyAsValueID(CSSPropertyTextAlign); |
| 2163 | if (!value) |
| 2164 | return; |
| 2165 | |
| 2166 | auto newValue = CSSValueInvalid; |
| 2167 | switch (*value) { |
| 2168 | case CSSValueStart: |
| 2169 | case CSSValueEnd: |
| 2170 | switch (direction) { |
| 2171 | case WritingDirection::Natural: |
| 2172 | return; |
| 2173 | case WritingDirection::LeftToRight: |
| 2174 | newValue = CSSValueLeft; |
| 2175 | break; |
| 2176 | case WritingDirection::RightToLeft: |
| 2177 | newValue = CSSValueRight; |
| 2178 | break; |
| 2179 | default: |
| 2180 | ASSERT_NOT_REACHED()((void)0); |
| 2181 | return; |
| 2182 | } |
| 2183 | break; |
| 2184 | case CSSValueLeft: |
| 2185 | case CSSValueWebkitLeft: |
| 2186 | newValue = CSSValueRight; |
| 2187 | break; |
| 2188 | case CSSValueRight: |
| 2189 | case CSSValueWebkitRight: |
| 2190 | newValue = CSSValueLeft; |
| 2191 | break; |
| 2192 | case CSSValueCenter: |
| 2193 | case CSSValueWebkitCenter: |
| 2194 | case CSSValueJustify: |
| 2195 | return; |
| 2196 | default: |
| 2197 | ASSERT_NOT_REACHED()((void)0); |
| 2198 | return; |
| 2199 | } |
| 2200 | |
| 2201 | auto isTextControl = [](Element* focusedElement) { |
| 2202 | if (RefPtr input = dynamicDowncast<HTMLInputElement>(focusedElement)) |
| 2203 | return input->isTextField() || input->isSearchField(); |
| 2204 | return is<HTMLTextAreaElement>(focusedElement); |
| 2205 | }; |
| 2206 | |
| 2207 | if (RefPtr focusedElement = document->focusedElement(); isTextControl(focusedElement.get())) { |
| 2208 | if (direction != WritingDirection::Natural) { |
| 2209 | focusedElement->setAttributeWithoutSynchronization(alignAttr, nameString(newValue)); |
| 2210 | document->updateStyleIfNeeded(); |
| 2211 | } |
| 2212 | return; |
| 2213 | } |
| 2214 | |
| 2215 | Ref style = MutableStyleProperties::create(); |
| 2216 | style->setProperty(CSSPropertyTextAlign, newValue); |
| 2217 | applyParagraphStyle(style.ptr()); |
| 2218 | } |
| 2219 | |
| 2220 | void Editor::setBaseWritingDirection(WritingDirection direction) |
| 2221 | { |
| 2222 | Ref document = this->document(); |
| 2223 | #if PLATFORM(IOS_FAMILY)(defined 1 && 1) |
| 2224 | if (inSameParagraph(document->selection().selection().visibleStart(), document->selection().selection().visibleEnd()) |
| 2225 | && baseWritingDirectionForSelectionStart() == direction) |
| 2226 | return; |
| 2227 | #endif |
| 2228 | |
| 2229 | if (RefPtr focusedElement = dynamicDowncast<HTMLTextFormControlElement>(document->focusedElement()); focusedElement && focusedElement->isTextField()) { |
| 2230 | if (direction == WritingDirection::Natural) |
| 2231 | return; |
| 2232 | |
| 2233 | auto directionValue = direction == WritingDirection::LeftToRight ? "ltr"_s : "rtl"_s; |
| 2234 | auto writingDirectionInputTypeName = inputTypeNameForEditingAction(EditAction::SetBlockWritingDirection); |
| 2235 | if (!dispatchBeforeInputEvent(*focusedElement, writingDirectionInputTypeName, IsInputMethodComposing::No, directionValue)) |
| 2236 | return; |
| 2237 | |
| 2238 | focusedElement->setAttributeWithoutSynchronization(dirAttr, directionValue); |
| 2239 | dispatchInputEvent(*focusedElement, writingDirectionInputTypeName, IsInputMethodComposing::No, directionValue); |
| 2240 | document->updateStyleIfNeeded(); |
| 2241 | return; |
| 2242 | } |
| 2243 | |
| 2244 | auto style = MutableStyleProperties::create(); |
| 2245 | style->setProperty(CSSPropertyDirection, direction == WritingDirection::LeftToRight ? "ltr"_s : direction == WritingDirection::RightToLeft ? "rtl"_s : "inherit"_s); |
| 2246 | applyParagraphStyleToSelection(style.ptr(), EditAction::SetBlockWritingDirection); |
| 2247 | } |
| 2248 | |
| 2249 | WritingDirection Editor::baseWritingDirectionForSelectionStart() const |
| 2250 | { |
| 2251 | auto result = WritingDirection::LeftToRight; |
| 2252 | |
| 2253 | Position pos = document().selection().selection().visibleStart().deepEquivalent(); |
| 2254 | RefPtr node = pos.deprecatedNode(); |
| 2255 | if (!node) |
| 2256 | return result; |
| 2257 | |
| 2258 | ScriptDisallowedScope::InMainThread scriptDisallowedScope; |
| 2259 | |
| 2260 | CheckedPtr renderer = node->renderer(); |
| 2261 | if (renderer && !is<RenderBlock>(*renderer)) |
| 2262 | renderer = renderer->containingBlock(); |
| 2263 | if (!renderer) |
| 2264 | return result; |
| 2265 | |
| 2266 | switch (downcast<RenderBlock>(*renderer).writingMode().bidiDirection()) { |
| 2267 | case TextDirection::LTR: |
| 2268 | return WritingDirection::LeftToRight; |
| 2269 | case TextDirection::RTL: |
| 2270 | return WritingDirection::RightToLeft; |
| 2271 | } |
| 2272 | |
| 2273 | return result; |
| 2274 | } |
| 2275 | |
| 2276 | void Editor::selectComposition() |
| 2277 | { |
| 2278 | auto range = compositionRange(); |
| 2279 | if (!range) |
| 2280 | return; |
| 2281 | |
| 2282 | // The composition can start inside a composed character sequence, so we have to override checks. |
| 2283 | // See <http://bugs.webkit.org/show_bug.cgi?id=15781> |
| 2284 | VisibleSelection selection; |
| 2285 | selection.setWithoutValidation(makeDeprecatedLegacyPosition(range->start), makeDeprecatedLegacyPosition(range->end)); |
| 2286 | document().selection().setSelection(selection, { }); |
| 2287 | } |
| 2288 | |
| 2289 | Node* Editor::nodeBeforeWritingSuggestions() const |
| 2290 | { |
| 2291 | Ref document = this->document(); |
| 2292 | if (!document->selection().isCaret()) |
| 2293 | return nullptr; |
| 2294 | |
| 2295 | auto position = document->selection().selection().end(); |
| 2296 | RefPtr container = position.containerNode(); |
| 2297 | if (!container) |
| 2298 | return nullptr; |
| 2299 | |
| 2300 | if (auto* text = dynamicDowncast<Text>(container.get())) |
| 2301 | return text; |
| 2302 | |
| 2303 | return position.computeNodeBeforePosition(); |
| 2304 | } |
| 2305 | |
| 2306 | Element* Editor::writingSuggestionsContainerElement() const |
| 2307 | { |
| 2308 | RefPtr node = nodeBeforeWritingSuggestions(); |
| 2309 | if (!node) |
| 2310 | return nullptr; |
| 2311 | |
| 2312 | return node->parentElement(); |
| 2313 | } |
| 2314 | |
| 2315 | void Editor::removeWritingSuggestionIfNeeded() |
| 2316 | { |
| 2317 | Ref document = this->document(); |
| 2318 | document->updateStyleIfNeeded(); |
| 2319 | |
| 2320 | m_customCompositionAnnotations = { }; |
| 2321 | m_isHandlingAcceptedCandidate = false; |
| 2322 | |
| 2323 | RefPtr selectedElement = writingSuggestionsContainerElement(); |
| 2324 | if (!selectedElement) |
| 2325 | return; |
| 2326 | |
| 2327 | m_writingSuggestionData = nullptr; |
| 2328 | selectedElement->invalidateStyleAndRenderersForSubtree(); |
| 2329 | } |
| 2330 | |
| 2331 | void Editor::confirmComposition() |
| 2332 | { |
| 2333 | #if PLATFORM(MAC)(defined WTF_PLATFORM_MAC && WTF_PLATFORM_MAC) |
| 2334 | if (m_isHandlingAcceptedCandidate) { |
| 2335 | removeWritingSuggestionIfNeeded(); |
| 2336 | return; |
| 2337 | } |
| 2338 | #endif |
| 2339 | |
| 2340 | if (!m_compositionNode) |
| 2341 | return; |
| 2342 | setComposition(compositionText(), ConfirmComposition); |
| 2343 | } |
| 2344 | |
| 2345 | String Editor::compositionText() const |
| 2346 | { |
| 2347 | if (!m_compositionNode) |
| 2348 | return { }; |
| 2349 | |
| 2350 | return protect(compositionNode())->data().substring(m_compositionStart, m_compositionEnd - m_compositionStart); |
| 2351 | } |
| 2352 | |
| 2353 | bool Editor::hasDeadKeyComposition() const |
| 2354 | { |
| 2355 | if (!m_compositionNode) |
| 2356 | return false; |
| 2357 | |
| 2358 | if (m_compositionStart + 1 != m_compositionEnd) |
| 2359 | return false; |
| 2360 | |
| 2361 | auto compositionText = this->compositionText(); |
| 2362 | return compositionText.length() == 1 && isLetterOrSymbolModifier(compositionText[0]); |
| 2363 | } |
| 2364 | |
| 2365 | void Editor::confirmOrCancelCompositionAndNotifyClient() |
| 2366 | { |
| 2367 | if (!hasComposition()) |
| 2368 | return; |
| 2369 | |
| 2370 | Ref document = this->document(); |
| 2371 | RefPtr frame = document->frame(); |
| 2372 | if (!frame) |
| 2373 | return; |
| 2374 | |
| 2375 | if (cancelCompositionIfSelectionIsInvalid()) |
| 2376 | return; |
| 2377 | |
| 2378 | confirmComposition(); |
| 2379 | |
| 2380 | if (CheckedPtr editorClient = client()) { |
| 2381 | editorClient->respondToChangedSelection(frame.get()); |
| 2382 | editorClient->discardedComposition(document); |
| 2383 | } |
| 2384 | } |
| 2385 | |
| 2386 | void Editor::cancelComposition() |
| 2387 | { |
| 2388 | #if PLATFORM(MAC)(defined WTF_PLATFORM_MAC && WTF_PLATFORM_MAC) |
| 2389 | if (m_isHandlingAcceptedCandidate) { |
| 2390 | removeWritingSuggestionIfNeeded(); |
| 2391 | return; |
| 2392 | } |
| 2393 | #endif |
| 2394 | |
| 2395 | if (!m_compositionNode) |
| 2396 | return; |
| 2397 | setComposition(emptyString(), CancelComposition); |
| 2398 | } |
| 2399 | |
| 2400 | bool Editor::cancelCompositionIfSelectionIsInvalid() |
| 2401 | { |
| 2402 | unsigned start; |
| 2403 | unsigned end; |
| 2404 | if (!hasComposition() || ignoreSelectionChanges() || getCompositionSelection(start, end)) |
| 2405 | return false; |
| 2406 | |
| 2407 | cancelComposition(); |
| 2408 | return true; |
| 2409 | } |
| 2410 | |
| 2411 | void Editor::confirmComposition(const String& text) |
| 2412 | { |
| 2413 | setComposition(text, ConfirmComposition); |
| 2414 | } |
| 2415 | |
| 2416 | class SetCompositionScope { |
| 2417 | public: |
| 2418 | SetCompositionScope(Ref<Document>&& document) |
| 2419 | : m_document(WTF::move(document)) |
| 2420 | , m_typingGestureIndicator(*m_document->frame()) |
| 2421 | { |
| 2422 | m_document->editor().setIgnoreSelectionChanges(true); |
| 2423 | } |
| 2424 | |
| 2425 | ~SetCompositionScope() |
| 2426 | { |
| 2427 | m_document->editor().setIgnoreSelectionChanges(false); |
| 2428 | if (CheckedPtr editorClient = m_document->editor().client()) |
| 2429 | editorClient->didUpdateComposition(); |
| 2430 | } |
| 2431 | |
| 2432 | RefPtr<Document> m_document; |
| 2433 | UserTypingGestureIndicator m_typingGestureIndicator; |
| 2434 | }; |
| 2435 | |
| 2436 | void Editor::setComposition(const String& text, SetCompositionMode mode) |
| 2437 | { |
| 2438 | ASSERT(mode == ConfirmComposition || mode == CancelComposition)((void)0); |
| 2439 | Ref document = this->document(); |
| 2440 | SetCompositionScope setCompositionScope(document.copyRef()); |
| 2441 | |
| 2442 | if (mode == CancelComposition) |
| 2443 | ASSERT(text == emptyString())((void)0); |
| 2444 | else |
| 2445 | selectComposition(); |
| 2446 | |
| 2447 | RefPtr previousCompositionNode = m_compositionNode; |
| 2448 | m_compositionNode = nullptr; |
| 2449 | m_customCompositionUnderlines.clear(); |
| 2450 | m_customCompositionHighlights.clear(); |
| 2451 | m_customCompositionAnnotations.clear(); |
| 2452 | |
| 2453 | if (CheckedPtr cache = document->existingAXObjectCache(); cache && previousCompositionNode) |
| 2454 | cache->onTextCompositionChange(*previousCompositionNode, CompositionState::Ended, false, text, m_compositionStart, m_isHandlingAcceptedCandidate); |
| 2455 | |
| 2456 | if (document->selection().isNone()) |
| 2457 | return; |
| 2458 | |
| 2459 | // Always delete the current composition before inserting the finalized composition text if we're confirming our composition. |
| 2460 | // Our default behavior (if the beforeinput event is not prevented) is to insert the finalized composition text back in. |
| 2461 | // We pass TypingCommand::TextCompositionType::Pending here to indicate that we are deleting the pending composition. |
| 2462 | if (mode != CancelComposition) |
| 2463 | TypingCommand::deleteSelection(document.copyRef(), { }, TypingCommand::TextCompositionType::Pending); |
| 2464 | |
| 2465 | insertTextForConfirmedComposition(text); |
| 2466 | |
| 2467 | if (RefPtr target = document->focusedElement()) |
| 2468 | target->dispatchEvent(CompositionEvent::create(eventNames().compositionendEvent, document->windowProxy(), text)); |
| 2469 | |
| 2470 | if (mode == CancelComposition) { |
| 2471 | // An open typing command that disagrees about current selection would cause issues with typing later on. |
| 2472 | TypingCommand::closeTyping(document); |
| 2473 | } |
| 2474 | } |
| 2475 | |
| 2476 | void Editor::closeTyping() |
| 2477 | { |
| 2478 | TypingCommand::closeTyping(m_document); |
| 2479 | } |
| 2480 | |
| 2481 | RenderInline* Editor::writingSuggestionRenderer() const |
| 2482 | { |
| 2483 | return m_writingSuggestionRenderer.get(); |
| 2484 | } |
| 2485 | |
| 2486 | void Editor::setWritingSuggestionRenderer(RenderInline& renderer) |
| 2487 | { |
| 2488 | m_writingSuggestionRenderer = renderer; |
| 2489 | } |
| 2490 | |
| 2491 | #if PLATFORM(COCOA)(defined 1 && 1) |
| 2492 | void Editor::setWritingSuggestion(const String& fullTextWithPrediction, const CharacterRange& selection) |
| 2493 | { |
| 2494 | Ref document = this->document(); |
| 2495 | document->updateStyleIfNeeded(); |
| 2496 | |
| 2497 | RefPtr selectedElement = writingSuggestionsContainerElement(); |
| 2498 | if (!selectedElement) |
| 2499 | return; |
| 2500 | |
| 2501 | if (!selectedElement->hasEditableStyle()) |
| 2502 | return; |
| 2503 | |
| 2504 | auto range = document->selection().selection().firstRange(); |
| 2505 | if (!range) |
| 2506 | return; |
| 2507 | |
| 2508 | if (!range->collapsed()) |
| 2509 | return; |
| 2510 | |
| 2511 | if (!is<Text>(range->startContainer())) |
| 2512 | return; |
| 2513 | |
| 2514 | range->start.offset = 0; |
| 2515 | |
| 2516 | m_isHandlingAcceptedCandidate = true; |
| 2517 | |
| 2518 | auto newText = fullTextWithPrediction.substring(0, selection.location); |
| 2519 | auto suggestionText = fullTextWithPrediction.substring(selection.location); |
| 2520 | |
| 2521 | auto currentText = m_writingSuggestionData ? m_writingSuggestionData->currentText() : emptyString(); |
| 2522 | |
| 2523 | ASSERT(newText.isEmpty() || newText.startsWith(currentText))((void)0); |
| 2524 | auto textDelta = newText.isEmpty() ? emptyString() : newText.substring(currentText.length()); |
| 2525 | |
| 2526 | auto offset = range->endOffset(); |
| 2527 | auto offsetWithDelta = currentText.isEmpty() ? offset : offset + textDelta.length(); |
| 2528 | |
| 2529 | if (!suggestionText.isEmpty()) { |
| 2530 | String originalPrefix; |
| 2531 | String originalSuffix; |
| 2532 | if (m_writingSuggestionData) { |
| 2533 | originalPrefix = m_writingSuggestionData->originalPrefix(); |
| 2534 | originalSuffix = m_writingSuggestionData->originalSuffix(); |
| 2535 | } else { |
| 2536 | originalPrefix = WebCore::plainTextReplacingNoBreakSpace(*range); |
| 2537 | originalSuffix = suggestionText; |
| 2538 | } |
| 2539 | |
| 2540 | m_writingSuggestionData = makeUnique<WritingSuggestionData>(WTF::move(suggestionText), WTF::move(newText), WTF::move(offsetWithDelta), WTF::move(originalPrefix), WTF::move(originalSuffix), Editor::writingSuggestionsSupportsSuffix()); |
| 2541 | } else |
| 2542 | m_writingSuggestionData = nullptr; |
| 2543 | |
| 2544 | if (!currentText.isEmpty()) { |
| 2545 | SetForScope isInsertingTextForWritingSuggestionScope { m_isInsertingTextForWritingSuggestion, true }; |
| 2546 | insertText(textDelta, nullptr, TextEventInputKeyboard); |
| 2547 | } else |
| 2548 | selectedElement->invalidateStyleAndRenderersForSubtree(); |
| 2549 | } |
| 2550 | #endif |
| 2551 | |
| 2552 | void Editor::setComposition(const String& text, const Vector<CompositionUnderline>& underlines, const Vector<CompositionHighlight>& highlights, const HashMap<String, Vector<CharacterRange>>& annotations, unsigned selectionStart, unsigned selectionEnd) |
| 2553 | { |
| 2554 | Ref document = this->document(); |
| 2555 | SetCompositionScope setCompositionScope(document.copyRef()); |
| 2556 | |
| 2557 | // Updates styles before setting selection for composition to prevent |
| 2558 | // inserting the previous composition text into text nodes oddly. |
| 2559 | // See https://bugs.webkit.org/show_bug.cgi?id=46868 |
| 2560 | document->updateStyleIfNeeded(); |
| 2561 | |
| 2562 | selectComposition(); |
| 2563 | |
| 2564 | if (document->selection().isNone()) |
| 2565 | return; |
| 2566 | |
| 2567 | String originalText = selectedText(); |
| 2568 | bool isStartingToRecomposeExistingRange = !text.isEmpty() && selectionStart < selectionEnd && !hasComposition(); |
| 2569 | if (isStartingToRecomposeExistingRange) { |
| 2570 | // We pass TypingCommand::TextCompositionType::Final here to indicate that we are removing composition text that has been finalized. |
| 2571 | TypingCommand::deleteSelection(document.copyRef(), { }, TypingCommand::TextCompositionType::Final); |
| 2572 | const VisibleSelection& currentSelection = document->selection().selection(); |
| 2573 | if (currentSelection.isRange()) { |
| 2574 | // If deletion was prevented, then we need to collapse the selection to the end so that the original text will not be recomposed. |
| 2575 | document->selection().setSelection({ currentSelection.end(), currentSelection.end() }); |
| 2576 | } |
| 2577 | } |
| 2578 | |
| 2579 | #if PLATFORM(IOS_FAMILY)(defined 1 && 1) |
| 2580 | client()->startDelayingAndCoalescingContentChangeNotifications(); |
| 2581 | #endif |
| 2582 | |
| 2583 | RefPtr<CompositionEvent> event; |
| 2584 | RefPtr target { document->focusedElement() }; |
| 2585 | if (target) { |
| 2586 | // Dispatch an appropriate composition event to the focused node. |
| 2587 | // We check the composition status and choose an appropriate composition event since this |
| 2588 | // function is used for three purposes: |
| 2589 | // 1. Starting a new composition. |
| 2590 | // Send a compositionstart and a compositionupdate event when this function creates |
| 2591 | // a new composition node, i.e. |
| 2592 | // m_compositionNode == 0 && !text.isEmpty(). |
| 2593 | // Sending a compositionupdate event at this time ensures that at least one |
| 2594 | // compositionupdate event is dispatched. |
| 2595 | // 2. Updating the existing composition node. |
| 2596 | // Send a compositionupdate event when this function updates the existing composition |
| 2597 | // node, i.e. m_compositionNode != 0 && !text.isEmpty(). |
| 2598 | // 3. Canceling the ongoing composition. |
| 2599 | // Send a compositionend event when function deletes the existing composition node, i.e. |
| 2600 | // m_compositionNode != 0 && test.isEmpty(). |
| 2601 | if (!m_compositionNode) { |
| 2602 | // We should send a compositionstart event only when the given text is not empty because this |
| 2603 | // function doesn't create a composition node when the text is empty. |
| 2604 | if (!text.isEmpty()) { |
| 2605 | // When an inline predicition is being offered, there will be text and a non-zero amount of highlights. |
| 2606 | m_isHandlingAcceptedCandidate = !highlights.isEmpty(); |
| 2607 | |
| 2608 | target->dispatchEvent(CompositionEvent::create(eventNames().compositionstartEvent, document->windowProxy(), originalText)); |
| 2609 | event = CompositionEvent::create(eventNames().compositionupdateEvent, document->windowProxy(), text); |
| 2610 | } |
| 2611 | } else if (!text.isEmpty()) |
| 2612 | event = CompositionEvent::create(eventNames().compositionupdateEvent, document->windowProxy(), text); |
| 2613 | |
| 2614 | if (event) |
| 2615 | target->dispatchEvent(*event); |
| 2616 | } |
| 2617 | |
| 2618 | // If text is empty, then delete the old composition here. If text is non-empty, InsertTextCommand::input |
| 2619 | // will delete the old composition with an optimized replace operation. |
| 2620 | if (text.isEmpty()) { |
| 2621 | // The absence of text implies that there are currently no inline predicitions being offered. |
| 2622 | m_isHandlingAcceptedCandidate = false; |
| 2623 | |
| 2624 | TypingCommand::deleteSelection(document.copyRef(), TypingCommand::Option::PreventSpellChecking, TypingCommand::TextCompositionType::Pending); |
| 2625 | if (target) |
| 2626 | target->dispatchEvent(CompositionEvent::create(eventNames().compositionendEvent, document->windowProxy(), text)); |
| 2627 | } |
| 2628 | |
| 2629 | RefPtr previousCompositionNode = m_compositionNode; |
| 2630 | m_compositionNode = nullptr; |
| 2631 | m_customCompositionUnderlines.clear(); |
| 2632 | m_customCompositionHighlights.clear(); |
| 2633 | m_customCompositionAnnotations.clear(); |
| 2634 | |
| 2635 | if (!text.isEmpty()) { |
| 2636 | TypingCommand::insertText(document.copyRef(), text, event.get(), OptionSet { TypingCommand::Option::SelectInsertedText, TypingCommand::Option::PreventSpellChecking }, TypingCommand::TextCompositionType::Pending); |
| 2637 | |
| 2638 | // Find out what node has the composition now. |
| 2639 | Position base = document->selection().selection().base().downstream(); |
| 2640 | Position extent = document->selection().selection().extent(); |
| 2641 | RefPtr baseNode = base.deprecatedNode(); |
| 2642 | unsigned baseOffset = base.deprecatedEditingOffset(); |
| 2643 | RefPtr extentNode = extent.deprecatedNode(); |
| 2644 | unsigned extentOffset = extent.deprecatedEditingOffset(); |
| 2645 | |
| 2646 | if (RefPtr baseTextNode = dynamicDowncast<Text>(baseNode); baseTextNode && baseNode == extentNode && baseOffset + text.length() == extentOffset) { |
| 2647 | m_compositionNode = baseTextNode; |
| 2648 | m_compositionStart = baseOffset; |
| 2649 | m_compositionEnd = extentOffset; |
| 2650 | m_customCompositionUnderlines = underlines; |
| 2651 | for (auto& underline : m_customCompositionUnderlines) { |
| 2652 | underline.startOffset += baseOffset; |
| 2653 | underline.endOffset += baseOffset; |
| 2654 | } |
| 2655 | m_customCompositionHighlights = highlights; |
| 2656 | for (auto& highlight : m_customCompositionHighlights) { |
| 2657 | highlight.startOffset += baseOffset; |
| 2658 | highlight.endOffset += baseOffset; |
| 2659 | } |
| 2660 | |
| 2661 | m_customCompositionAnnotations = annotations; |
| 2662 | for (auto it = m_customCompositionAnnotations.begin(); it != m_customCompositionAnnotations.end(); ++it) { |
| 2663 | for (auto& range : it->value) |
| 2664 | range.location += baseOffset; |
| 2665 | } |
| 2666 | |
| 2667 | if (CheckedPtr renderer = baseTextNode->renderer()) |
| 2668 | renderer->repaint(); |
| 2669 | |
| 2670 | unsigned start = std::min(baseOffset + selectionStart, extentOffset); |
| 2671 | unsigned end = std::min(std::max(start, baseOffset + selectionEnd), extentOffset); |
| 2672 | auto range = SimpleRange { { *baseTextNode, start }, { *baseTextNode, end } }; |
| 2673 | document->selection().setSelectedRange(range, Affinity::Downstream, FrameSelection::ShouldCloseTyping::No); |
| 2674 | } |
| 2675 | } |
| 2676 | |
| 2677 | if (CheckedPtr cache = document->existingAXObjectCache()) { |
| 2678 | if (previousCompositionNode && previousCompositionNode != m_compositionNode) { |
| 2679 | auto state = m_compositionNode ? CompositionState::InProgress : CompositionState::Ended; |
| 2680 | cache->onTextCompositionChange(*previousCompositionNode, state, true, text, m_compositionStart, m_isHandlingAcceptedCandidate); |
| 2681 | } |
| 2682 | if (RefPtr compositionNode = this->compositionNode()) { |
| 2683 | auto state = previousCompositionNode ? CompositionState::InProgress : CompositionState::Started; |
| 2684 | cache->onTextCompositionChange(*compositionNode, state, true, text, m_compositionStart, m_isHandlingAcceptedCandidate); |
| 2685 | } |
| 2686 | } |
| 2687 | |
| 2688 | #if PLATFORM(IOS_FAMILY)(defined 1 && 1) |
| 2689 | client()->stopDelayingAndCoalescingContentChangeNotifications(); |
| 2690 | #endif |
| 2691 | } |
| 2692 | |
| 2693 | void Editor::ignoreSpelling() |
| 2694 | { |
| 2695 | if (!client()) |
| 2696 | return; |
| 2697 | |
| 2698 | if (auto selectedRange = document().selection().selection().toNormalizedRange()) |
| 2699 | removeMarkers(*selectedRange, DocumentMarkerType::Spelling); |
| 2700 | |
| 2701 | String text = selectedText(); |
| 2702 | ASSERT(text.length())((void)0); |
| 2703 | textChecker()->ignoreWordInSpellDocument(text); |
| 2704 | } |
| 2705 | |
| 2706 | void Editor::learnSpelling() |
| 2707 | { |
| 2708 | if (!client()) |
| 2709 | return; |
| 2710 | |
| 2711 | // FIXME: On macOS, when use "learn" button on "Spelling and Grammar" panel, we don't call this function. It should remove misspelling markers around the learned word, see <rdar://problem/5396072>. |
| 2712 | |
| 2713 | if (auto selectedRange = document().selection().selection().toNormalizedRange()) |
| 2714 | removeMarkers(*selectedRange, DocumentMarkerType::Spelling); |
| 2715 | |
| 2716 | String text = selectedText(); |
| 2717 | ASSERT(text.length())((void)0); |
| 2718 | textChecker()->learnWord(text); |
| 2719 | } |
| 2720 | |
| 2721 | #if !PLATFORM(IOS_FAMILY)(defined 1 && 1) |
| 2722 | |
| 2723 | void Editor::advanceToNextMisspelling(bool startBeforeSelection) |
| 2724 | { |
| 2725 | Ref document = this->document(); |
| 2726 | |
| 2727 | // The basic approach is to search in two phases: from the selection end to the end of the document, and |
| 2728 | // then we wrap and search from the document start to (approximately) where we started. |
| 2729 | |
| 2730 | // Start at the end of the selection, search to edge of document. |
| 2731 | // Starting at the selection end makes repeated "check spelling" commands work. |
| 2732 | VisibleSelection selection(document->selection().selection()); |
| 2733 | auto spellingSearchRange = makeRangeSelectingNodeContents(document); |
| 2734 | |
| 2735 | bool startedWithSelection = false; |
| 2736 | if (selection.start().deprecatedNode()) { |
| 2737 | startedWithSelection = true; |
| 2738 | if (startBeforeSelection) { |
| 2739 | // Match AppKit's rule: Start 1 character before the selection. |
| 2740 | auto start = selection.visibleStart(); |
| 2741 | auto beforeStart = start.previous(); |
| 2742 | if (beforeStart.isNotNull()) |
| 2743 | start = beforeStart; |
| 2744 | spellingSearchRange.start = *makeBoundaryPoint(start); |
| 2745 | } else |
| 2746 | spellingSearchRange.start = *makeBoundaryPoint(selection.visibleEnd()); |
| 2747 | } |
| 2748 | |
| 2749 | auto position = makeDeprecatedLegacyPosition(spellingSearchRange.start); |
| 2750 | if (!isEditablePosition(position)) { |
| 2751 | // This shouldn't happen in very often because the Spelling menu items aren't enabled unless the |
| 2752 | // selection is editable. |
| 2753 | // This can happen in Mail for a mix of non-editable and editable content (like Stationary), |
| 2754 | // when spell checking the whole document before sending the message. |
| 2755 | // In that case the document might not be editable, but there are editable pockets that need to be spell checked. |
| 2756 | |
| 2757 | position = VisiblePosition(firstEditablePositionAfterPositionInRoot(position, document->documentElement())).deepEquivalent(); |
| 2758 | if (position.isNull()) |
| 2759 | return; |
| 2760 | |
| 2761 | if (auto point = makeBoundaryPoint(position.parentAnchoredEquivalent())) |
| 2762 | spellingSearchRange.start = *point; |
| 2763 | startedWithSelection = false; // won't need to wrap |
| 2764 | } |
| 2765 | |
| 2766 | // topNode defines the whole range we want to operate on |
| 2767 | RefPtr topNode { highestEditableRoot(position) }; |
| 2768 | if (topNode) |
| 2769 | spellingSearchRange.end = makeBoundaryPointAfterNodeContents(*topNode); |
| 2770 | |
| 2771 | // If spellingSearchRange starts in the middle of a word, advance to the next word so we start checking |
| 2772 | // at a word boundary. Going back by one char and then forward by a word does the trick. |
| 2773 | if (startedWithSelection) { |
| 2774 | auto oneBeforeStart = VisiblePosition(makeContainerOffsetPosition(spellingSearchRange.start)).previous(); |
| 2775 | if (oneBeforeStart.isNotNull()) |
| 2776 | spellingSearchRange.start = *makeBoundaryPoint(endOfWord(oneBeforeStart)); |
| 2777 | // else we were already at the start of the editable node |
| 2778 | } |
| 2779 | |
| 2780 | if (spellingSearchRange.collapsed()) |
| 2781 | return; // nothing to search in |
| 2782 | |
| 2783 | // Get the spell checker if it is available |
| 2784 | if (!client()) |
| 2785 | return; |
| 2786 | |
| 2787 | // We go to the end of our first range instead of the start of it, just to be sure |
| 2788 | // we don't get foiled by any word boundary problems at the start. It means we might |
| 2789 | // do a tiny bit more searching. |
| 2790 | auto searchEndAfterWrap = spellingSearchRange.end; |
| 2791 | |
| 2792 | TextCheckingHelper::MisspelledWord misspelledWord; |
| 2793 | TextCheckingHelper::UngrammaticalPhrase ungrammaticalPhrase; |
| 2794 | auto grammarSearchRange = spellingSearchRange; |
| 2795 | |
| 2796 | if (unifiedTextCheckerEnabled()) { |
| 2797 | auto foundItem = TextCheckingHelper(*client(), spellingSearchRange).findFirstMisspelledWordOrUngrammaticalPhrase(isGrammarCheckingEnabled()); |
| 2798 | if (auto* word = std::get_if<TextCheckingHelper::MisspelledWord>(&foundItem)) |
| 2799 | misspelledWord = WTF::move(*word); |
| 2800 | else |
| 2801 | ungrammaticalPhrase = std::get<TextCheckingHelper::UngrammaticalPhrase>(WTF::move(foundItem)); |
| 2802 | } else { |
| 2803 | misspelledWord = TextCheckingHelper(*client(), spellingSearchRange).findFirstMisspelledWord(); |
| 2804 | |
| 2805 | if (!misspelledWord.word.isEmpty()) { |
| 2806 | // Stop looking at start of next misspelled word |
| 2807 | CharacterIterator characterIterator(grammarSearchRange); |
| 2808 | characterIterator.advance(misspelledWord.offset); |
| 2809 | grammarSearchRange.end = characterIterator.range().start; |
| 2810 | } |
| 2811 | |
| 2812 | if (isGrammarCheckingEnabled()) |
| 2813 | ungrammaticalPhrase = TextCheckingHelper(*client(), grammarSearchRange).findFirstUngrammaticalPhrase(); |
| 2814 | } |
| 2815 | |
| 2816 | // If we found neither bad grammar nor a misspelled word, wrap and try again (but don't bother if we started at the beginning of the |
| 2817 | // block rather than at a selection). |
| 2818 | if (startedWithSelection && misspelledWord.word.isEmpty() && ungrammaticalPhrase.phrase.isEmpty()) { |
| 2819 | if (topNode) |
| 2820 | spellingSearchRange.start = makeBoundaryPointBeforeNodeContents(*topNode); |
| 2821 | spellingSearchRange.end = searchEndAfterWrap; |
| 2822 | |
| 2823 | if (unifiedTextCheckerEnabled()) { |
| 2824 | auto foundItem = TextCheckingHelper(*client(), spellingSearchRange).findFirstMisspelledWordOrUngrammaticalPhrase(isGrammarCheckingEnabled()); |
| 2825 | if (auto* word = std::get_if<TextCheckingHelper::MisspelledWord>(&foundItem)) |
| 2826 | misspelledWord = WTF::move(*word); |
| 2827 | else |
| 2828 | ungrammaticalPhrase = std::get<TextCheckingHelper::UngrammaticalPhrase>(WTF::move(foundItem)); |
| 2829 | } else { |
| 2830 | misspelledWord = TextCheckingHelper(*client(), spellingSearchRange).findFirstMisspelledWord(); |
| 2831 | |
| 2832 | grammarSearchRange = spellingSearchRange; |
| 2833 | if (!misspelledWord.word.isEmpty()) { |
| 2834 | // Stop looking at start of next misspelled word |
| 2835 | CharacterIterator characterIterator(grammarSearchRange); |
| 2836 | characterIterator.advance(misspelledWord.offset); |
| 2837 | grammarSearchRange.end = characterIterator.range().start; |
| 2838 | } |
| 2839 | |
| 2840 | if (isGrammarCheckingEnabled()) |
| 2841 | ungrammaticalPhrase = TextCheckingHelper(*client(), grammarSearchRange).findFirstUngrammaticalPhrase(); |
| 2842 | } |
| 2843 | } |
| 2844 | |
| 2845 | if (!ungrammaticalPhrase.phrase.isEmpty()) { |
| 2846 | // We found bad grammar. Since we only searched for bad grammar up to the first misspelled word, the bad grammar |
| 2847 | // takes precedence and we ignore any potential misspelled word. Select the grammar detail, update the spelling |
| 2848 | // panel, and store a marker so we draw the green squiggle later. |
| 2849 | |
| 2850 | ASSERT(ungrammaticalPhrase.phrase.length() > 0)((void)0); |
| 2851 | ASSERT(ungrammaticalPhrase.detail.range.length > 0)((void)0); |
| 2852 | |
| 2853 | // FIXME 4859190: This gets confused with doubled punctuation at the end of a paragraph. |
| 2854 | auto badGrammarRange = resolveCharacterRange(grammarSearchRange, { ungrammaticalPhrase.offset + ungrammaticalPhrase.detail.range.location, ungrammaticalPhrase.detail.range.length }); |
| 2855 | document->selection().setSelection(VisibleSelection(badGrammarRange)); |
| 2856 | document->selection().revealSelection(); |
| 2857 | |
| 2858 | client()->updateSpellingUIWithGrammarString(ungrammaticalPhrase.phrase, ungrammaticalPhrase.detail); |
| 2859 | addMarker(badGrammarRange, DocumentMarkerType::Grammar, ungrammaticalPhrase.detail.userDescription); |
| 2860 | } else if (!misspelledWord.word.isEmpty()) { |
| 2861 | // We found a misspelling, but not any earlier bad grammar. Select the misspelling, update the spelling panel, and store |
| 2862 | // a marker so we draw the red squiggle later. |
| 2863 | |
| 2864 | auto misspellingRange = resolveCharacterRange(spellingSearchRange, { misspelledWord.offset, misspelledWord.word.length() }); |
| 2865 | document->selection().setSelection(VisibleSelection(misspellingRange)); |
| 2866 | document->selection().revealSelection(); |
| 2867 | |
| 2868 | client()->updateSpellingUIWithMisspelledWord(misspelledWord.word); |
| 2869 | addMarker(misspellingRange, DocumentMarkerType::Spelling); |
| 2870 | } |
| 2871 | } |
| 2872 | |
| 2873 | #endif // !PLATFORM(IOS_FAMILY) |
| 2874 | |
| 2875 | String Editor::misspelledWordAtCaretOrRange(Node* clickedNode) const |
| 2876 | { |
| 2877 | if (!isContinuousSpellCheckingEnabled() || !clickedNode || !isSpellCheckingEnabledFor(clickedNode)) |
| 2878 | return String(); |
| 2879 | |
| 2880 | VisibleSelection selection = document().selection().selection(); |
| 2881 | if (!selection.isContentEditable() || selection.isNone()) |
| 2882 | return String(); |
| 2883 | |
| 2884 | VisibleSelection wordSelection(selection.base()); |
| 2885 | wordSelection.expandUsingGranularity(TextGranularity::WordGranularity); |
| 2886 | auto wordRange = wordSelection.toNormalizedRange(); |
| 2887 | if (!wordRange) |
| 2888 | return String(); |
| 2889 | |
| 2890 | // In compliance with GTK+ applications, additionally allow to provide suggestions when the current |
| 2891 | // selection exactly match the word selection. |
| 2892 | if (selection.isRange() && *wordRange != selection.toNormalizedRange()) |
| 2893 | return String(); |
| 2894 | |
| 2895 | String word = plainText(*wordRange); |
| 2896 | if (word.isEmpty() || !client()) |
| 2897 | return String(); |
| 2898 | |
| 2899 | int wordLength = word.length(); |
| 2900 | int misspellingLocation = -1; |
| 2901 | int misspellingLength = 0; |
| 2902 | textChecker()->checkSpellingOfString(word, &misspellingLocation, &misspellingLength); |
| 2903 | |
| 2904 | return misspellingLength == wordLength ? word : String(); |
| 2905 | } |
| 2906 | |
| 2907 | String Editor::misspelledSelectionString() const |
| 2908 | { |
| 2909 | String selectedString = selectedText(); |
| 2910 | int length = selectedString.length(); |
| 2911 | if (!length || !client()) |
| 2912 | return String(); |
| 2913 | |
| 2914 | int misspellingLocation = -1; |
| 2915 | int misspellingLength = 0; |
| 2916 | textChecker()->checkSpellingOfString(selectedString, &misspellingLocation, &misspellingLength); |
| 2917 | |
| 2918 | // The selection only counts as misspelled if the selected text is exactly one misspelled word |
| 2919 | if (misspellingLength != length) |
| 2920 | return String(); |
| 2921 | |
| 2922 | // Update the spelling panel to be displaying this error (whether or not the spelling panel is on screen). |
| 2923 | // This is necessary to make a subsequent call to [NSSpellChecker ignoreWord:inSpellDocumentWithTag:] work |
| 2924 | // correctly; that call behaves differently based on whether the spelling panel is displaying a misspelling |
| 2925 | // or a grammar error. |
| 2926 | client()->updateSpellingUIWithMisspelledWord(selectedString); |
| 2927 | |
| 2928 | return selectedString; |
| 2929 | } |
| 2930 | |
| 2931 | Vector<String> Editor::guessesForMisspelledWord(const String& word) const |
| 2932 | { |
| 2933 | ASSERT(word.length())((void)0); |
| 2934 | |
| 2935 | Vector<String> guesses; |
| 2936 | if (client()) |
| 2937 | textChecker()->getGuessesForWord(word, String(), document().selection().selection(), guesses); |
| 2938 | return guesses; |
| 2939 | } |
| 2940 | |
| 2941 | TextCheckingGuesses Editor::guessesForMisspelledOrUngrammatical() |
| 2942 | { |
| 2943 | if (unifiedTextCheckerEnabled()) { |
| 2944 | std::optional<SimpleRange> range; |
| 2945 | auto selection = document().selection().selection(); |
| 2946 | if (selection.isCaret() && behavior().shouldAllowSpellingSuggestionsWithoutSelection()) { |
| 2947 | auto wordSelection = VisibleSelection(selection.base()); |
| 2948 | wordSelection.expandUsingGranularity(TextGranularity::WordGranularity); |
| 2949 | range = wordSelection.toNormalizedRange(); |
| 2950 | } else |
| 2951 | range = selection.toNormalizedRange(); |
| 2952 | if (!range || !client()) |
| 2953 | return { }; |
| 2954 | return TextCheckingHelper(*client(), *range).guessesForMisspelledWordOrUngrammaticalPhrase(isGrammarCheckingEnabled()); |
| 2955 | } |
| 2956 | |
| 2957 | auto misspelledWord = behavior().shouldAllowSpellingSuggestionsWithoutSelection() ? misspelledWordAtCaretOrRange(document().focusedElement()) : misspelledSelectionString(); |
| 2958 | if (misspelledWord.isEmpty()) |
| 2959 | return { }; |
| 2960 | return { guessesForMisspelledWord(misspelledWord), true, false }; |
| 2961 | } |
| 2962 | |
| 2963 | void Editor::showSpellingGuessPanel() |
| 2964 | { |
| 2965 | if (!client()) { |
| 2966 | LOG_ERROR("No NSSpellChecker")((void)0); |
| 2967 | return; |
| 2968 | } |
| 2969 | |
| 2970 | if (client()->spellingUIIsShowing()) { |
| 2971 | client()->showSpellingUI(false); |
| 2972 | return; |
| 2973 | } |
| 2974 | |
| 2975 | #if !PLATFORM(IOS_FAMILY)(defined 1 && 1) |
| 2976 | advanceToNextMisspelling(true); |
| 2977 | #endif |
| 2978 | client()->showSpellingUI(true); |
| 2979 | } |
| 2980 | |
| 2981 | bool Editor::spellingPanelIsShowing() |
| 2982 | { |
| 2983 | if (!client()) |
| 2984 | return false; |
| 2985 | return client()->spellingUIIsShowing(); |
| 2986 | } |
| 2987 | |
| 2988 | void Editor::clearMisspellingsAndBadGrammar(const VisibleSelection& movingSelection) |
| 2989 | { |
| 2990 | if (auto selectedRange = movingSelection.toNormalizedRange()) |
| 2991 | removeMarkers(*selectedRange, { DocumentMarkerType::Spelling, DocumentMarkerType::Grammar }); |
| 2992 | } |
| 2993 | |
| 2994 | void Editor::markMisspellingsAndBadGrammar(const VisibleSelection &movingSelection) |
| 2995 | { |
| 2996 | markMisspellingsAndBadGrammar(movingSelection, isContinuousSpellCheckingEnabled() && isGrammarCheckingEnabled(), movingSelection); |
| 2997 | } |
| 2998 | |
| 2999 | void Editor::markMisspellingsAfterTypingToWord(const VisiblePosition& wordStart, const VisibleSelection& selectionAfterTyping, AllowTextReplacement allowTextReplacement) |
| 3000 | { |
| 3001 | Ref document = this->document(); |
| 3002 | |
| 3003 | if (platformOrClientDrivenTextCheckerEnabled()) |
| 3004 | return; |
| 3005 | |
| 3006 | #if PLATFORM(IOS_FAMILY)(defined 1 && 1) |
| 3007 | UNUSED_PARAM(selectionAfterTyping)(void)selectionAfterTyping; |
| 3008 | UNUSED_PARAM(allowTextReplacement)(void)allowTextReplacement; |
| 3009 | OptionSet<TextCheckingType> textCheckingOptions; |
| 3010 | if (isContinuousSpellCheckingEnabled()) |
| 3011 | textCheckingOptions.add(TextCheckingType::Spelling); |
| 3012 | if (!textCheckingOptions.contains(TextCheckingType::Spelling)) |
| 3013 | return; |
| 3014 | |
| 3015 | auto adjacentWords = VisibleSelection(startOfWord(wordStart, WordSide::LeftWordIfOnBoundary), endOfWord(wordStart, WordSide::RightWordIfOnBoundary)); |
| 3016 | auto adjacentWordRange = adjacentWords.toNormalizedRange(); |
| 3017 | |
| 3018 | #if ENABLE(POST_EDITING_GRAMMAR_CHECKING)(defined 1 && 1) |
| 3019 | if (isGrammarCheckingEnabled()) { |
| 3020 | textCheckingOptions.add(TextCheckingType::Grammar); |
| 3021 | textCheckingOptions.add(TextCheckingType::Correction); |
| 3022 | auto sentenceStart = startOfSentence(wordStart); |
| 3023 | auto sentenceEnd = endOfSentence(wordStart); |
| 3024 | VisibleSelection fullSentence(sentenceStart, sentenceEnd); |
| 3025 | auto fullSentenceRange = fullSentence.toNormalizedRange(); |
| 3026 | if (!fullSentenceRange) |
| 3027 | return; |
| 3028 | |
| 3029 | markAllMisspellingsAndBadGrammarInRanges(textCheckingOptions, adjacentWordRange, adjacentWordRange, fullSentenceRange); |
| 3030 | } else |
| 3031 | #endif |
| 3032 | { |
| 3033 | markAllMisspellingsAndBadGrammarInRanges(textCheckingOptions, adjacentWordRange, adjacentWordRange, adjacentWordRange); |
| 3034 | } |
| 3035 | #else |
| 3036 | #if !USE(AUTOMATIC_TEXT_REPLACEMENT)(defined USE_AUTOMATIC_TEXT_REPLACEMENT && USE_AUTOMATIC_TEXT_REPLACEMENT ) |
| 3037 | UNUSED_PARAM(allowTextReplacement)(void)allowTextReplacement; |
| 3038 | #endif |
| 3039 | |
| 3040 | if (unifiedTextCheckerEnabled()) { |
| 3041 | m_alternativeTextController->applyPendingCorrection(selectionAfterTyping); |
| 3042 | |
| 3043 | OptionSet<TextCheckingType> textCheckingOptions; |
| 3044 | |
| 3045 | if (isContinuousSpellCheckingEnabled()) |
| 3046 | textCheckingOptions.add(TextCheckingType::Spelling); |
| 3047 | |
| 3048 | #if USE(AUTOMATIC_TEXT_REPLACEMENT)(defined USE_AUTOMATIC_TEXT_REPLACEMENT && USE_AUTOMATIC_TEXT_REPLACEMENT ) |
| 3049 | if (allowTextReplacement == AllowTextReplacement::Yes |
| 3050 | && (isAutomaticQuoteSubstitutionEnabled() |
| 3051 | || isAutomaticLinkDetectionEnabled() |
| 3052 | || isAutomaticDashSubstitutionEnabled() |
| 3053 | || isAutomaticTextReplacementEnabled() |
| 3054 | || (textCheckingOptions.contains(TextCheckingType::Spelling) && isAutomaticSpellingCorrectionEnabled()))) |
| 3055 | textCheckingOptions.add(TextCheckingType::Replacement); |
| 3056 | #endif |
| 3057 | if (!textCheckingOptions.contains(TextCheckingType::Spelling) && !textCheckingOptions.contains(TextCheckingType::Replacement)) |
| 3058 | return; |
| 3059 | |
| 3060 | if (isGrammarCheckingEnabled()) |
| 3061 | textCheckingOptions.add(TextCheckingType::Grammar); |
| 3062 | |
| 3063 | auto sentenceStart = startOfSentence(wordStart); |
| 3064 | auto sentenceEnd = endOfSentence(wordStart); |
| 3065 | VisibleSelection fullSentence(sentenceStart, sentenceEnd); |
| 3066 | auto fullSentenceRange = fullSentence.toNormalizedRange(); |
| 3067 | if (!fullSentenceRange) |
| 3068 | return; |
| 3069 | |
| 3070 | auto spellCheckingStart = wordStart; |
| 3071 | auto spellCheckingEnd = wordStart; |
| 3072 | |
| 3073 | // FIXME: The following logic doesn't handle adding spelling markers due to retro sentence corrections when an |
| 3074 | // incorrectly spelled range is separated from the start of the current word by a text node inside an element |
| 3075 | // with spellcheck disabled. To fix this, we need to refactor markAllMisspellingsAndBadGrammarInRanges so that |
| 3076 | // it can handle a list of spelling ranges, alongside the grammar range. |
| 3077 | while (sentenceStart < spellCheckingStart) { |
| 3078 | auto previousPosition = spellCheckingStart.previous(CannotCrossEditingBoundary); |
| 3079 | if (previousPosition.isNull() || previousPosition == spellCheckingStart) |
| 3080 | break; |
| 3081 | |
| 3082 | RefPtr container = previousPosition.deepEquivalent().downstream().containerNode(); |
| 3083 | RefPtr containerElement = dynamicDowncast<Element>(container); |
| 3084 | if (!containerElement) |
| 3085 | containerElement = container->parentElement(); |
| 3086 | if (containerElement && !containerElement->isSpellCheckingEnabled()) |
| 3087 | break; |
| 3088 | |
| 3089 | spellCheckingStart = previousPosition; |
| 3090 | } |
| 3091 | |
| 3092 | while (spellCheckingEnd < sentenceEnd) { |
| 3093 | auto nextPosition = spellCheckingEnd.next(CannotCrossEditingBoundary); |
| 3094 | if (nextPosition.isNull() || nextPosition == spellCheckingEnd) |
| 3095 | break; |
| 3096 | |
| 3097 | if (RefPtr containerElement = nextPosition.deepEquivalent().upstream().containerOrParentElement()) { |
| 3098 | if (!containerElement->isSpellCheckingEnabled()) |
| 3099 | break; |
| 3100 | } |
| 3101 | |
| 3102 | spellCheckingEnd = nextPosition; |
| 3103 | } |
| 3104 | |
| 3105 | auto spellCheckingRange = VisibleSelection(spellCheckingStart, spellCheckingEnd).toNormalizedRange(); |
| 3106 | if (!spellCheckingRange) |
| 3107 | return; |
| 3108 | |
| 3109 | auto adjacentWordRange = intersection(VisibleSelection(startOfWord(wordStart, WordSide::LeftWordIfOnBoundary), endOfWord(wordStart, WordSide::RightWordIfOnBoundary)).toNormalizedRange(), fullSentenceRange); |
| 3110 | if (!adjacentWordRange) |
| 3111 | return; |
| 3112 | |
| 3113 | // The spelling and grammar markers in these ranges are recomputed. This is because typing a word may |
| 3114 | // cause any other part of the current sentence to lose or gain spelling correction markers, due to |
| 3115 | // sentence retro correction. As such, we expand the spell checking range to encompass as much of the |
| 3116 | // full sentence as we can, respecting boundaries where spellchecking is disabled. |
| 3117 | removeMarkers(*fullSentenceRange, DocumentMarkerType::Grammar); |
| 3118 | removeMarkers(*spellCheckingRange, DocumentMarkerType::Spelling); |
| 3119 | markAllMisspellingsAndBadGrammarInRanges(textCheckingOptions, spellCheckingRange, adjacentWordRange, fullSentenceRange); |
| 3120 | return; |
| 3121 | } |
| 3122 | |
| 3123 | if (!isContinuousSpellCheckingEnabled()) |
| 3124 | return; |
| 3125 | |
| 3126 | // Check spelling of one word |
| 3127 | auto misspellingRange = markMisspellings(VisibleSelection(startOfWord(wordStart, WordSide::LeftWordIfOnBoundary), endOfWord(wordStart, WordSide::RightWordIfOnBoundary))); |
| 3128 | |
| 3129 | // Autocorrect the misspelled word. |
| 3130 | if (!misspellingRange) |
| 3131 | return; |
| 3132 | |
| 3133 | if (!isGrammarCheckingEnabled()) |
| 3134 | return; |
| 3135 | |
| 3136 | // Check grammar of entire sentence |
| 3137 | markBadGrammar(VisibleSelection(startOfSentence(wordStart), endOfSentence(wordStart))); |
| 3138 | #endif |
| 3139 | } |
| 3140 | |
| 3141 | std::optional<SimpleRange> Editor::markMisspellingsOrBadGrammar(const VisibleSelection& selection, bool checkSpelling) |
| 3142 | { |
| 3143 | #if !PLATFORM(IOS_FAMILY)(defined 1 && 1) |
| 3144 | // This function is called with a selection already expanded to word boundaries. |
| 3145 | // Might be nice to assert that here. |
| 3146 | |
| 3147 | // This function is used only for as-you-type checking, so if that's off we do nothing. Note that |
| 3148 | // grammar checking can only be on if spell checking is also on. |
| 3149 | if (!isContinuousSpellCheckingEnabled()) |
| 3150 | return std::nullopt; |
| 3151 | |
| 3152 | auto searchRange = selection.toNormalizedRange(); |
| 3153 | if (!searchRange) |
| 3154 | return std::nullopt; |
| 3155 | |
| 3156 | // If we're not in an editable node, bail. |
| 3157 | Ref editableNode = searchRange->startContainer(); |
| 3158 | if (!editableNode->hasEditableStyle()) |
| 3159 | return std::nullopt; |
| 3160 | |
| 3161 | if (!isSpellCheckingEnabledFor(editableNode.ptr())) |
| 3162 | return std::nullopt; |
| 3163 | |
| 3164 | // Get the spell checker if it is available |
| 3165 | if (!client()) |
| 3166 | return std::nullopt; |
| 3167 | |
| 3168 | TextCheckingHelper checker(*client(), *searchRange); |
| 3169 | if (checkSpelling) |
| 3170 | return checker.markAllMisspelledWords(); |
| 3171 | if (isGrammarCheckingEnabled()) |
| 3172 | checker.markAllUngrammaticalPhrases(); |
| 3173 | return std::nullopt; |
| 3174 | #else |
| 3175 | UNUSED_PARAM(selection)(void)selection; |
| 3176 | UNUSED_PARAM(checkSpelling)(void)checkSpelling; |
| 3177 | return std::nullopt; |
| 3178 | #endif |
| 3179 | } |
| 3180 | |
| 3181 | bool Editor::isSpellCheckingEnabledFor(Node* node) const |
| 3182 | { |
| 3183 | if (!node) |
| 3184 | return false; |
| 3185 | RefPtr element = dynamicDowncast<Element>(*node); |
| 3186 | if (!element) |
| 3187 | element = node->parentElement(); |
| 3188 | if (!element) |
| 3189 | return false; |
| 3190 | if (element->isInUserAgentShadowTree()) { |
| 3191 | if (RefPtr textControl = enclosingTextFormControl(firstPositionInOrBeforeNode(element.get()))) |
| 3192 | return textControl->isSpellCheckingEnabled(); |
| 3193 | } |
| 3194 | return element->isSpellCheckingEnabled(); |
| 3195 | } |
| 3196 | |
| 3197 | bool Editor::isSpellCheckingEnabledInFocusedNode() const |
| 3198 | { |
| 3199 | return isSpellCheckingEnabledFor(document().selection().selection().start().deprecatedNode()); |
| 3200 | } |
| 3201 | |
| 3202 | std::optional<SimpleRange> Editor::markMisspellings(const VisibleSelection& selection) |
| 3203 | { |
| 3204 | return markMisspellingsOrBadGrammar(selection, true); |
| 3205 | } |
| 3206 | |
| 3207 | void Editor::markBadGrammar(const VisibleSelection& selection) |
| 3208 | { |
| 3209 | markMisspellingsOrBadGrammar(selection, false); |
| 3210 | } |
| 3211 | |
| 3212 | void Editor::markAllMisspellingsAndBadGrammarInRanges(OptionSet<TextCheckingType> textCheckingOptions, const std::optional<SimpleRange>& spellingRange, const std::optional<SimpleRange>& automaticReplacementRange, const std::optional<SimpleRange>& grammarRange) |
| 3213 | { |
| 3214 | if (platformOrClientDrivenTextCheckerEnabled()) |
| 3215 | return; |
| 3216 | |
| 3217 | ASSERT(unifiedTextCheckerEnabled())((void)0); |
| 3218 | |
| 3219 | // There shouldn't be pending autocorrection at this moment. |
| 3220 | ASSERT(!m_alternativeTextController->hasPendingCorrection())((void)0); |
| 3221 | |
| 3222 | bool shouldMarkGrammar = textCheckingOptions.contains(TextCheckingType::Grammar); |
| 3223 | bool shouldShowCorrectionPanel = textCheckingOptions.contains(TextCheckingType::ShowCorrectionPanel); |
| 3224 | |
| 3225 | // This function is called with selections already expanded to word boundaries. |
| 3226 | if (!client() || !spellingRange || (shouldMarkGrammar && !grammarRange)) |
| 3227 | return; |
| 3228 | |
| 3229 | // Do not mark spelling or grammer corrections when an inline prediction candidate is currently being offered. |
| 3230 | if (m_isHandlingAcceptedCandidate) |
| 3231 | return; |
| 3232 | |
| 3233 | // If we're not in an editable node, bail. |
| 3234 | Ref editableNode = spellingRange->startContainer(); |
| 3235 | if (!editableNode->hasEditableStyle()) |
| 3236 | return; |
| 3237 | |
| 3238 | if (!isSpellCheckingEnabledFor(editableNode.ptr())) |
| 3239 | return; |
| 3240 | |
| 3241 | auto& rangeToCheck = shouldMarkGrammar ? *grammarRange : *spellingRange; |
| 3242 | TextCheckingParagraph paragraphToCheck(rangeToCheck); |
| 3243 | if (paragraphToCheck.isEmpty()) |
| 3244 | return; |
| 3245 | |
| 3246 | bool asynchronous = document().settings().asynchronousSpellCheckingEnabled() && !shouldShowCorrectionPanel; |
| 3247 | |
| 3248 | // In asynchronous mode, we intentionally check paragraph-wide sentence. |
| 3249 | auto resolvedOptions = resolveTextCheckingTypeMask(editableNode, textCheckingOptions); |
| 3250 | auto& paragraphRange = paragraphToCheck.paragraphRange(); |
| 3251 | auto& checkingRange = asynchronous ? paragraphRange : rangeToCheck; |
| 3252 | auto& textReplacementRange = automaticReplacementRange ? *automaticReplacementRange : rangeToCheck; |
| 3253 | auto request = SpellCheckRequest::create(resolvedOptions, TextCheckingProcessType::TextCheckingProcessIncremental, checkingRange, textReplacementRange, paragraphRange); |
| 3254 | if (!request) |
| 3255 | return; |
| 3256 | |
| 3257 | if (asynchronous) { |
| 3258 | m_spellChecker->requestCheckingFor(request.releaseNonNull()); |
| 3259 | return; |
| 3260 | } |
| 3261 | |
| 3262 | Vector<TextCheckingResult> results; |
| 3263 | checkTextOfParagraph(*textChecker(), paragraphToCheck.text(), resolvedOptions, results, document().selection().selection()); |
| 3264 | markAndReplaceFor(request.releaseNonNull(), results); |
| 3265 | |
| 3266 | #if PLATFORM(COCOA)(defined 1 && 1) |
| 3267 | bool extendedProofreadingEnabled = document().settings().extendedProofreadingEnabled(); |
| 3268 | |
| 3269 | if (shouldMarkGrammar && extendedProofreadingEnabled) { |
| 3270 | RefPtr extendedRequest = SpellCheckRequest::create(resolvedOptions, TextCheckingProcessType::TextCheckingProcessIncremental, checkingRange, textReplacementRange, paragraphRange); |
| 3271 | if (extendedRequest) |
| 3272 | m_spellChecker->requestExtendedCheckingFor(extendedRequest.releaseNonNull(), results); |
| 3273 | } |
| 3274 | #endif |
| 3275 | } |
| 3276 | |
| 3277 | static bool isAutomaticTextReplacementType(TextCheckingType type) |
| 3278 | { |
| 3279 | switch (type) { |
| 3280 | case TextCheckingType::None: |
| 3281 | case TextCheckingType::Spelling: |
| 3282 | case TextCheckingType::Grammar: |
| 3283 | return false; |
| 3284 | case TextCheckingType::Link: |
| 3285 | case TextCheckingType::Quote: |
| 3286 | case TextCheckingType::Dash: |
| 3287 | case TextCheckingType::Replacement: |
| 3288 | case TextCheckingType::Correction: |
| 3289 | case TextCheckingType::ShowCorrectionPanel: |
| 3290 | return true; |
| 3291 | } |
| 3292 | ASSERT_NOT_REACHED()((void)0); |
| 3293 | return false; |
| 3294 | } |
| 3295 | |
| 3296 | void Editor::replaceRangeForSpellChecking(const SimpleRange& rangeToReplace, const String& replacement) |
| 3297 | { |
| 3298 | SpellingCorrectionCommand::create(rangeToReplace, replacement)->apply(); |
| 3299 | } |
| 3300 | |
| 3301 | static void correctSpellcheckingPreservingTextCheckingParagraph(TextCheckingParagraph& paragraph, const SimpleRange& rangeToReplace, const String& replacement, CharacterRange resultCharacterRange) |
| 3302 | { |
| 3303 | Ref scopeNode = downcast<ContainerNode>(paragraph.paragraphRange().startContainer().rootNode()); |
| 3304 | auto paragraphCharacterRange = characterRange(makeBoundaryPointBeforeNodeContents(scopeNode), paragraph.paragraphRange()); |
| 3305 | |
| 3306 | SpellingCorrectionCommand::create(rangeToReplace, replacement)->apply(); |
| 3307 | |
| 3308 | // TextCheckingParagraph may be orphaned after SpellingCorrectionCommand mutated DOM. |
| 3309 | // See <rdar://10305315>, http://webkit.org/b/89526. |
| 3310 | |
| 3311 | paragraphCharacterRange.length += replacement.length() - resultCharacterRange.length; |
| 3312 | auto newParagraphRange = resolveCharacterRange(makeRangeSelectingNodeContents(scopeNode), paragraphCharacterRange); |
| 3313 | auto spellCheckingRange = resolveCharacterRange(newParagraphRange, { resultCharacterRange.location, replacement.length() }); |
| 3314 | paragraph = TextCheckingParagraph(spellCheckingRange, spellCheckingRange, newParagraphRange); |
| 3315 | } |
| 3316 | |
| 3317 | void Editor::markAndReplaceFor(const SpellCheckRequest& request, const Vector<TextCheckingResult>& results) |
| 3318 | { |
| 3319 | Ref document = this->document(); |
| 3320 | |
| 3321 | auto textCheckingOptions = request.data().checkingTypes(); |
| 3322 | TextCheckingParagraph paragraph(request.checkingRange(), request.automaticReplacementRange(), request.paragraphRange()); |
| 3323 | |
| 3324 | const bool shouldPerformReplacement = textCheckingOptions.containsAny({ TextCheckingType::Quote, TextCheckingType::Dash, TextCheckingType::Replacement }); |
| 3325 | const bool shouldMarkSpelling = textCheckingOptions.contains(TextCheckingType::Spelling); |
| 3326 | const bool shouldMarkGrammar = textCheckingOptions.contains(TextCheckingType::Grammar); |
| 3327 | const bool shouldMarkLink = textCheckingOptions.contains(TextCheckingType::Link); |
| 3328 | const bool shouldShowCorrectionPanel = textCheckingOptions.contains(TextCheckingType::ShowCorrectionPanel); |
| 3329 | const bool shouldCheckForCorrection = shouldShowCorrectionPanel || textCheckingOptions.contains(TextCheckingType::Correction); |
| 3330 | #if !USE(AUTOCORRECTION_PANEL)(defined USE_AUTOCORRECTION_PANEL && USE_AUTOCORRECTION_PANEL ) |
| 3331 | ASSERT(!shouldShowCorrectionPanel)((void)0); |
| 3332 | #endif |
| 3333 | |
| 3334 | // Expand the range to encompass entire paragraphs, since text checking needs that much context. |
| 3335 | uint64_t selectionOffset = 0; |
| 3336 | bool useAmbiguousBoundaryOffset = false; |
| 3337 | bool selectionChanged = false; |
| 3338 | bool restoreSelectionAfterChange = false; |
| 3339 | |
| 3340 | if (shouldPerformReplacement || shouldMarkSpelling || shouldCheckForCorrection) { |
| 3341 | if (document->selection().isCaret()) { |
| 3342 | // Attempt to save the caret position so we can restore it later if needed |
| 3343 | Position caretPosition = document->selection().selection().end(); |
| 3344 | selectionOffset = paragraph.offsetTo(caretPosition).releaseReturnValue(); |
| 3345 | restoreSelectionAfterChange = true; |
| 3346 | if (selectionOffset > 0 && selectionOffset <= paragraph.text().length() && isAmbiguousBoundaryCharacter(paragraph.text()[selectionOffset - 1])) |
| 3347 | useAmbiguousBoundaryOffset = true; |
| 3348 | } |
| 3349 | } |
| 3350 | |
| 3351 | int offsetDueToReplacement = 0; |
| 3352 | Vector<CharacterRange> previousGrammarRanges; |
| 3353 | |
| 3354 | for (unsigned i = 0; i < results.size(); i++) { |
| 3355 | auto spellingRangeEndOffset = paragraph.checkingEnd() + offsetDueToReplacement; |
| 3356 | if (!results[i].type.hasExactlyOneBitSet()) { |
| 3357 | ASSERT_NOT_REACHED()((void)0); |
| 3358 | continue; |
| 3359 | } |
| 3360 | TextCheckingType resultType = *results[i].type.toSingleValue(); |
| 3361 | auto resultLocation = results[i].range.location + offsetDueToReplacement; |
| 3362 | auto resultLength = results[i].range.length; |
| 3363 | auto resultEndLocation = resultLocation + resultLength; |
| 3364 | auto automaticReplacementStartLocation = paragraph.automaticReplacementStart(); |
| 3365 | auto automaticReplacementEndLocation = automaticReplacementStartLocation + paragraph.automaticReplacementLength() + offsetDueToReplacement; |
| 3366 | const String& replacement = results[i].replacement; |
| 3367 | bool resultEndsAtAmbiguousBoundary = useAmbiguousBoundaryOffset && selectionOffset - 1 <= resultEndLocation; |
| 3368 | |
| 3369 | bool resultRangeIsAcceptableForReplacement = automaticReplacementStartLocation <= resultEndLocation && resultEndLocation <= automaticReplacementEndLocation; |
| 3370 | // In this case the result range just has to touch the automatic replacement range, so we can handle replacing non-word text such as punctuation. |
| 3371 | #if ENABLE(POST_EDITING_GRAMMAR_CHECKING)(defined 1 && 1) |
| 3372 | if (!resultRangeIsAcceptableForReplacement && shouldCheckForCorrection && resultType == TextCheckingType::Correction) |
| 3373 | resultRangeIsAcceptableForReplacement = results[i].details.size() > 0; |
| 3374 | if (!resultRangeIsAcceptableForReplacement && shouldMarkGrammar && shouldCheckForCorrection && resultType == TextCheckingType::Correction) { |
| 3375 | resultRangeIsAcceptableForReplacement = previousGrammarRanges.containsIf([&](auto& range) { |
| 3376 | return range.location == resultLocation && range.length == resultLength; |
| 3377 | }); |
| 3378 | } |
| 3379 | #endif |
| 3380 | |
| 3381 | // Only mark misspelling if: |
| 3382 | // 1. Current text checking isn't done for autocorrection, in which case shouldMarkSpelling is false. |
| 3383 | // 2. Result falls within spellingRange. |
| 3384 | // 3. The word in question doesn't end at an ambiguous boundary. For instance, we would not mark |
| 3385 | // "wouldn'" as misspelled right after apostrophe is typed. |
| 3386 | if (shouldMarkSpelling && !shouldShowCorrectionPanel && resultType == TextCheckingType::Spelling |
| 3387 | && resultLocation >= paragraph.checkingStart() && resultEndLocation <= spellingRangeEndOffset && !resultEndsAtAmbiguousBoundary) { |
| 3388 | ASSERT(resultLength > 0)((void)0); |
| 3389 | auto misspellingRange = paragraph.subrange({ resultLocation, resultLength }); |
| 3390 | if (!m_alternativeTextController->isSpellingMarkerAllowed(misspellingRange)) |
| 3391 | continue; |
| 3392 | addMarker(misspellingRange, DocumentMarkerType::Spelling, replacement); |
| 3393 | } else if (shouldMarkGrammar && resultType == TextCheckingType::Grammar && paragraph.checkingRangeCovers({ resultLocation, resultLength })) { |
| 3394 | ASSERT(resultLength > 0)((void)0); |
| 3395 | for (auto& detail : results[i].details) { |
| 3396 | ASSERT(detail.range.length > 0)((void)0); |
| 3397 | if (paragraph.checkingRangeCovers({ resultLocation + detail.range.location, detail.range.length })) { |
| 3398 | auto badGrammarRange = paragraph.subrange({ resultLocation + detail.range.location, detail.range.length }); |
| 3399 | addMarker(badGrammarRange, DocumentMarkerType::Grammar, detail.userDescription); |
| 3400 | previousGrammarRanges.append(CharacterRange(resultLocation + detail.range.location, detail.range.length)); |
| 3401 | } |
| 3402 | } |
| 3403 | } else if (resultRangeIsAcceptableForReplacement && isAutomaticTextReplacementType(resultType)) { |
| 3404 | ASSERT(resultLength > 0)((void)0); |
| 3405 | |
| 3406 | if (shouldShowCorrectionPanel && (resultEndLocation < automaticReplacementEndLocation |
| 3407 | || (resultType != TextCheckingType::Replacement && resultType != TextCheckingType::Correction))) |
| 3408 | continue; |
| 3409 | |
| 3410 | // Apply replacement if: |
| 3411 | // 1. The replacement length is non-zero. |
| 3412 | // 2. The result doesn't end at an ambiguous boundary. |
| 3413 | // (FIXME: this is required until 6853027 is fixed and text checking can do this for us |
| 3414 | bool doReplacement = replacement.length() > 0 && !resultEndsAtAmbiguousBoundary; |
| 3415 | auto rangeToReplace = paragraph.subrange({ resultLocation, resultLength }); |
| 3416 | |
| 3417 | // Adding links should be done only immediately after they are typed. |
| 3418 | if (resultType == TextCheckingType::Link && selectionOffset != resultEndLocation + 1) |
| 3419 | continue; |
| 3420 | |
| 3421 | if (!(shouldPerformReplacement || shouldCheckForCorrection || shouldMarkLink) || !doReplacement) |
| 3422 | continue; |
| 3423 | |
| 3424 | String replacedString = plainText(rangeToReplace); |
| 3425 | bool existingMarkersPermitReplacement = m_alternativeTextController->processMarkersOnTextToBeReplacedByResult(results[i], rangeToReplace, replacedString); |
| 3426 | #if ENABLE(POST_EDITING_GRAMMAR_CHECKING)(defined 1 && 1) |
| 3427 | if (!existingMarkersPermitReplacement && shouldCheckForCorrection && resultType == TextCheckingType::Correction) |
| 3428 | existingMarkersPermitReplacement = results[i].details.size() > 0; |
| 3429 | #endif |
| 3430 | if (!existingMarkersPermitReplacement) |
| 3431 | continue; |
| 3432 | |
| 3433 | if (shouldShowCorrectionPanel) { |
| 3434 | if (resultEndLocation == automaticReplacementEndLocation) { |
| 3435 | // We only show the correction panel on the last word. |
| 3436 | m_alternativeTextController->show(rangeToReplace, replacement); |
| 3437 | break; |
| 3438 | } |
| 3439 | // If this function is called for showing correction panel, we ignore other correction or replacement. |
| 3440 | continue; |
| 3441 | } |
| 3442 | |
| 3443 | VisibleSelection selectionToReplace(rangeToReplace); |
| 3444 | if (selectionToReplace != document->selection().selection()) { |
| 3445 | if (!document->selection().shouldChangeSelection(selectionToReplace)) |
| 3446 | continue; |
| 3447 | } |
| 3448 | |
| 3449 | if (resultType == TextCheckingType::Link) { |
| 3450 | document->selection().setSelection(selectionToReplace); |
| 3451 | selectionChanged = true; |
| 3452 | restoreSelectionAfterChange = false; |
| 3453 | if (canEditRichly()) |
| 3454 | CreateLinkCommand::create(document.copyRef(), replacement)->apply(); |
| 3455 | } else if (canEdit() && shouldInsertText(replacement, rangeToReplace, EditorInsertAction::Typed)) { |
| 3456 | correctSpellcheckingPreservingTextCheckingParagraph(paragraph, rangeToReplace, replacement, { resultLocation, resultLength }); |
| 3457 | |
| 3458 | if (CheckedPtr cache = document->existingAXObjectCache()) { |
| 3459 | if (RefPtr root = document->selection().selection().rootEditableElement()) |
| 3460 | cache->onAutocorrectionOccured(*root.get()); |
| 3461 | } |
| 3462 | |
| 3463 | // Skip all other results for the replaced text. |
| 3464 | while (i + 1 < results.size() && results[i + 1].range.location + offsetDueToReplacement <= resultLocation) |
| 3465 | i++; |
| 3466 | |
| 3467 | selectionChanged = true; |
| 3468 | offsetDueToReplacement += replacement.length() - resultLength; |
| 3469 | if (resultLocation < selectionOffset) |
| 3470 | selectionOffset += replacement.length() - resultLength; |
| 3471 | |
| 3472 | if (resultType == TextCheckingType::Correction) { |
| 3473 | auto replacementRange = paragraph.subrange({ resultLocation, replacement.length() }); |
| 3474 | m_alternativeTextController->recordAutocorrectionResponse(AutocorrectionResponse::Accepted, replacedString, replacementRange); |
| 3475 | |
| 3476 | // Add a marker so that corrections can easily be undone and won't be re-corrected. |
| 3477 | m_alternativeTextController->markCorrection(replacementRange, replacedString); |
| 3478 | } |
| 3479 | } |
| 3480 | } |
| 3481 | } |
| 3482 | |
| 3483 | if (selectionChanged) { |
| 3484 | TextCheckingParagraph extendedParagraph(WTF::move(paragraph)); |
| 3485 | // Restore the caret position if we have made any replacements |
| 3486 | extendedParagraph.expandRangeToNextEnd(); |
| 3487 | if (restoreSelectionAfterChange && selectionOffset <= extendedParagraph.rangeLength()) { |
| 3488 | auto selectionRange = extendedParagraph.subrange({ 0, selectionOffset }); |
| 3489 | document->selection().moveTo(makeContainerOffsetPosition(selectionRange.end), Affinity::Downstream); |
| 3490 | } else { |
| 3491 | // If this fails for any reason, the fallback is to go one position beyond the last replacement |
| 3492 | document->selection().moveTo(document->selection().selection().end()); |
| 3493 | document->selection().modify(FrameSelection::Alteration::Move, SelectionDirection::Forward, TextGranularity::CharacterGranularity); |
| 3494 | } |
| 3495 | } |
| 3496 | } |
| 3497 | |
| 3498 | void Editor::changeBackToReplacedString(const String& replacedString) |
| 3499 | { |
| 3500 | #if !PLATFORM(IOS_FAMILY)(defined 1 && 1) |
| 3501 | ASSERT(unifiedTextCheckerEnabled())((void)0); |
| 3502 | |
| 3503 | if (replacedString.isEmpty()) |
| 3504 | return; |
| 3505 | |
| 3506 | auto selection = selectedRange(); |
| 3507 | if (!selection || !shouldInsertText(replacedString, selection, EditorInsertAction::Pasted)) |
| 3508 | return; |
| 3509 | |
| 3510 | m_alternativeTextController->recordAutocorrectionResponse(AutocorrectionResponse::Reverted, replacedString, *selection); |
| 3511 | TextCheckingParagraph paragraph(*selection); |
| 3512 | replaceSelectionWithText(replacedString, SelectReplacement::No, SmartReplace::No, EditAction::Insert); |
| 3513 | auto changedRange = paragraph.subrange(CharacterRange(paragraph.checkingStart(), replacedString.length())); |
| 3514 | addMarker(changedRange, DocumentMarkerType::Replacement, String()); |
| 3515 | m_alternativeTextController->markReversed(changedRange); |
| 3516 | #else |
| 3517 | ASSERT_NOT_REACHED()((void)0); |
| 3518 | UNUSED_PARAM(replacedString)(void)replacedString; |
| 3519 | #endif // !PLATFORM(IOS_FAMILY) |
| 3520 | } |
| 3521 | |
| 3522 | |
| 3523 | void Editor::markMisspellingsAndBadGrammar(const VisibleSelection& spellingSelection, bool markGrammar, const VisibleSelection& grammarSelection) |
| 3524 | { |
| 3525 | if (platformOrClientDrivenTextCheckerEnabled()) |
| 3526 | return; |
| 3527 | |
| 3528 | if (unifiedTextCheckerEnabled()) { |
| 3529 | if (!isContinuousSpellCheckingEnabled()) |
| 3530 | return; |
| 3531 | |
| 3532 | // markMisspellingsAndBadGrammar() is triggered by selection change, in which case we check spelling and grammar, but don't autocorrect misspellings. |
| 3533 | OptionSet<TextCheckingType> textCheckingOptions { TextCheckingType::Spelling }; |
| 3534 | if (markGrammar && isGrammarCheckingEnabled()) |
| 3535 | textCheckingOptions.add(TextCheckingType::Grammar); |
| 3536 | auto spellCheckingRange = spellingSelection.toNormalizedRange(); |
| 3537 | markAllMisspellingsAndBadGrammarInRanges(textCheckingOptions, spellCheckingRange, spellCheckingRange, grammarSelection.toNormalizedRange()); |
| 3538 | return; |
| 3539 | } |
| 3540 | |
| 3541 | markMisspellings(spellingSelection); |
| 3542 | if (markGrammar) |
| 3543 | markBadGrammar(grammarSelection); |
| 3544 | } |
| 3545 | |
| 3546 | void Editor::unappliedSpellCorrection(const VisibleSelection& selectionOfCorrected, const String& corrected, const String& correction) |
| 3547 | { |
| 3548 | m_alternativeTextController->respondToUnappliedSpellCorrection(selectionOfCorrected, corrected, correction); |
| 3549 | } |
| 3550 | |
| 3551 | void Editor::updateMarkersForWordsAffectedByEditing(bool doNotRemoveIfSelectionAtWordBoundary) |
| 3552 | { |
| 3553 | Ref document = this->document(); |
| 3554 | CheckedPtr markers = document->markersIfExists(); |
| 3555 | if (!markers || !markers->hasMarkers()) |
| 3556 | return; |
| 3557 | |
| 3558 | if (!m_alternativeTextController->shouldRemoveMarkersUponEditing() && (!textChecker() || textChecker()->shouldEraseMarkersAfterChangeSelection(TextCheckingType::Spelling))) |
| 3559 | return; |
| 3560 | |
| 3561 | // We want to remove the markers from a word if an editing command will change the word. This can happen in one of |
| 3562 | // several scenarios: |
| 3563 | // 1. Insert in the middle of a word. |
| 3564 | // 2. Appending non whitespace at the beginning of word. |
| 3565 | // 3. Appending non whitespace at the end of word. |
| 3566 | // Note that, appending only whitespaces at the beginning or end of word won't change the word, so we don't need to |
| 3567 | // remove the markers on that word. |
| 3568 | // Of course, if current selection is a range, we potentially will edit two words that fall on the boundaries of |
| 3569 | // selection, and remove words between the selection boundaries. |
| 3570 | // |
| 3571 | VisiblePosition startOfSelection = document->selection().selection().start(); |
| 3572 | VisiblePosition endOfSelection = document->selection().selection().end(); |
| 3573 | if (startOfSelection.isNull()) |
| 3574 | return; |
| 3575 | // First word is the word that ends after or on the start of selection. |
| 3576 | VisiblePosition startOfFirstWord = startOfWord(startOfSelection, WordSide::LeftWordIfOnBoundary); |
| 3577 | VisiblePosition endOfFirstWord = endOfWord(startOfSelection, WordSide::LeftWordIfOnBoundary); |
| 3578 | // Last word is the word that begins before or on the end of selection |
| 3579 | VisiblePosition startOfLastWord = startOfWord(endOfSelection, WordSide::RightWordIfOnBoundary); |
| 3580 | VisiblePosition endOfLastWord = endOfWord(endOfSelection, WordSide::RightWordIfOnBoundary); |
| 3581 | |
| 3582 | if (startOfFirstWord.isNull()) { |
| 3583 | startOfFirstWord = startOfWord(startOfSelection, WordSide::RightWordIfOnBoundary); |
| 3584 | endOfFirstWord = endOfWord(startOfSelection, WordSide::RightWordIfOnBoundary); |
| 3585 | } |
| 3586 | |
| 3587 | if (endOfLastWord.isNull()) { |
| 3588 | startOfLastWord = startOfWord(endOfSelection, WordSide::LeftWordIfOnBoundary); |
| 3589 | endOfLastWord = endOfWord(endOfSelection, WordSide::LeftWordIfOnBoundary); |
| 3590 | } |
| 3591 | |
| 3592 | auto originalEndOfFirstWord = endOfFirstWord; |
| 3593 | auto originalStartOfLastWord = startOfLastWord; |
| 3594 | |
| 3595 | // If doNotRemoveIfSelectionAtWordBoundary is true, and first word ends at the start of selection, |
| 3596 | // we choose next word as the first word. |
| 3597 | if (doNotRemoveIfSelectionAtWordBoundary && endOfFirstWord == startOfSelection) { |
| 3598 | startOfFirstWord = nextWordPosition(startOfFirstWord); |
| 3599 | endOfFirstWord = endOfWord(startOfFirstWord, WordSide::RightWordIfOnBoundary); |
| 3600 | if (startOfFirstWord == originalStartOfLastWord) |
| 3601 | return; |
| 3602 | } |
| 3603 | |
| 3604 | // If doNotRemoveIfSelectionAtWordBoundary is true, and last word begins at the end of selection, |
| 3605 | // we choose previous word as the last word. |
| 3606 | if (doNotRemoveIfSelectionAtWordBoundary && startOfLastWord == endOfSelection) { |
| 3607 | startOfLastWord = previousWordPosition(startOfLastWord); |
| 3608 | endOfLastWord = endOfWord(startOfLastWord, WordSide::RightWordIfOnBoundary); |
| 3609 | if (endOfLastWord == originalEndOfFirstWord) |
| 3610 | return; |
| 3611 | } |
| 3612 | |
| 3613 | if (startOfFirstWord.isNull() || endOfFirstWord.isNull() || startOfLastWord.isNull() || endOfLastWord.isNull()) |
| 3614 | return; |
| 3615 | |
| 3616 | // Now we remove markers on everything between startOfFirstWord and endOfLastWord. |
| 3617 | // However, if an autocorrection change a single word to multiple words, we want to remove correction mark from all the |
| 3618 | // resulted words even we only edit one of them. For example, assuming autocorrection changes "avantgarde" to "avant |
| 3619 | // garde", we will have CorrectionIndicator marker on both words and on the whitespace between them. If we then edit garde, |
| 3620 | // we would like to remove the marker from word "avant" and whitespace as well. So we need to get the continous range of |
| 3621 | // of marker that contains the word in question, and remove marker on that whole range. |
| 3622 | auto wordRange = *makeSimpleRange(startOfFirstWord, endOfLastWord); |
| 3623 | |
| 3624 | for (auto& marker : markers->markersInRange(wordRange, DocumentMarkerType::DictationAlternatives)) |
| 3625 | m_alternativeTextController->removeDictationAlternativesForMarker(*marker); |
| 3626 | |
| 3627 | OptionSet<DocumentMarkerType> markerTypesToRemove { |
| 3628 | DocumentMarkerType::CorrectionIndicator, |
| 3629 | DocumentMarkerType::SpellCheckingExemption, |
| 3630 | DocumentMarkerType::Spelling, |
| 3631 | #if !PLATFORM(IOS_FAMILY)(defined 1 && 1) |
| 3632 | DocumentMarkerType::Grammar, |
| 3633 | #endif |
| 3634 | }; |
| 3635 | |
| 3636 | if (CheckedPtr client = this->client(); client && client->shouldRemoveDictationAlternativesAfterEditing()) |
| 3637 | markerTypesToRemove.add(DocumentMarkerType::DictationAlternatives); |
| 3638 | |
| 3639 | adjustMarkerTypesToRemoveForWordsAffectedByEditing(markerTypesToRemove); |
| 3640 | |
| 3641 | removeMarkers(wordRange, markerTypesToRemove, RemovePartiallyOverlappingMarker::Yes); |
| 3642 | markers->clearDescriptionOnMarkersIntersectingRange(wordRange, DocumentMarkerType::Replacement); |
| 3643 | } |
| 3644 | |
| 3645 | void Editor::deletedAutocorrectionAtPosition(const Position& position, const String& originalString) |
| 3646 | { |
| 3647 | m_alternativeTextController->deletedAutocorrectionAtPosition(position, originalString); |
| 3648 | } |
| 3649 | |
| 3650 | std::optional<SimpleRange> Editor::rangeForPoint(const IntPoint& windowPoint) |
| 3651 | { |
| 3652 | RefPtr document = this->document().frame()->documentAtPoint(windowPoint); |
| 3653 | if (!document) |
| 3654 | return std::nullopt; |
| 3655 | RefPtr frame = document->frame(); |
| 3656 | if (!frame) |
| 3657 | return std::nullopt; |
| 3658 | RefPtr frameView = frame->view(); |
| 3659 | if (!frameView) |
| 3660 | return std::nullopt; |
| 3661 | return VisibleSelection { frame->visiblePositionForPoint(frameView->windowToContents(windowPoint)) }.toNormalizedRange(); |
| 3662 | } |
| 3663 | |
| 3664 | void Editor::revealSelectionAfterEditingOperation(const ScrollAlignment& alignment, RevealExtentOption revealExtentOption) |
| 3665 | { |
| 3666 | if (m_ignoreSelectionChanges) |
| 3667 | return; |
| 3668 | |
| 3669 | SelectionRevealMode revealMode = SelectionRevealMode::Reveal; |
| 3670 | Ref document = this->document(); |
| 3671 | document->selection().revealSelection({ revealMode, alignment, revealExtentOption }); |
| 3672 | } |
| 3673 | |
| 3674 | void Editor::setIgnoreSelectionChanges(bool ignore, RevealSelection shouldRevealExistingSelection) |
| 3675 | { |
| 3676 | if (m_ignoreSelectionChanges == ignore) |
| 3677 | return; |
| 3678 | |
| 3679 | m_ignoreSelectionChanges = ignore; |
| 3680 | #if PLATFORM(IOS_FAMILY)(defined 1 && 1) |
| 3681 | // FIXME: Should suppress selection change notifications during a composition change <https://webkit.org/b/38830> |
| 3682 | if (!ignore) |
| 3683 | respondToChangedSelection(document().selection().selection(), { }); |
| 3684 | #endif |
| 3685 | if (!ignore && shouldRevealExistingSelection == RevealSelection::Yes) |
| 3686 | revealSelectionAfterEditingOperation(ScrollAlignment::alignToEdgeIfNeeded, RevealExtentOption::RevealExtent); |
| 3687 | } |
| 3688 | |
| 3689 | std::optional<SimpleRange> Editor::compositionRange() const |
| 3690 | { |
| 3691 | if (!m_compositionNode) |
| 3692 | return std::nullopt; |
| 3693 | unsigned length = m_compositionNode->length(); |
| 3694 | unsigned start = std::min(m_compositionStart, length); |
| 3695 | unsigned end = std::clamp(m_compositionEnd, start, length); |
| 3696 | if (start >= end) |
| 3697 | return std::nullopt; |
| 3698 | return { { { *m_compositionNode, start }, { *m_compositionNode, end } } }; |
| 3699 | } |
| 3700 | |
| 3701 | bool Editor::getCompositionSelection(unsigned& selectionStart, unsigned& selectionEnd) const |
| 3702 | { |
| 3703 | if (!m_compositionNode) |
| 3704 | return false; |
| 3705 | Ref document = this->document(); |
| 3706 | const VisibleSelection& selection = document->selection().selection(); |
| 3707 | Position start = selection.start(); |
| 3708 | if (start.deprecatedNode() != m_compositionNode) |
| 3709 | return false; |
| 3710 | Position end = selection.end(); |
| 3711 | if (end.deprecatedNode() != m_compositionNode) |
| 3712 | return false; |
| 3713 | |
| 3714 | if (static_cast<unsigned>(start.deprecatedEditingOffset()) < m_compositionStart) |
| 3715 | return false; |
| 3716 | if (static_cast<unsigned>(end.deprecatedEditingOffset()) > m_compositionEnd) |
| 3717 | return false; |
| 3718 | |
| 3719 | selectionStart = start.deprecatedEditingOffset() - m_compositionStart; |
| 3720 | selectionEnd = start.deprecatedEditingOffset() - m_compositionEnd; |
| 3721 | return true; |
| 3722 | } |
| 3723 | |
| 3724 | void Editor::transpose() |
| 3725 | { |
| 3726 | if (!canEdit()) |
| 3727 | return; |
| 3728 | |
| 3729 | Ref document = this->document(); |
| 3730 | VisibleSelection selection = document->selection().selection(); |
| 3731 | if (!selection.isCaret()) |
| 3732 | return; |
| 3733 | |
| 3734 | // Make a selection that goes back one character and forward two characters. |
| 3735 | VisiblePosition caret = selection.visibleStart(); |
| 3736 | VisiblePosition next = isEndOfParagraph(caret) ? caret : caret.next(); |
| 3737 | VisiblePosition previous = next.previous(); |
| 3738 | if (next == previous) |
| 3739 | return; |
| 3740 | previous = previous.previous(); |
| 3741 | if (!inSameParagraph(next, previous)) |
| 3742 | return; |
| 3743 | auto range = makeSimpleRange(previous, next); |
| 3744 | if (!range) |
| 3745 | return; |
| 3746 | VisibleSelection newSelection(*range); |
| 3747 | |
| 3748 | // Transpose the two characters. |
| 3749 | String text = plainText(*range); |
| 3750 | if (text.length() != 2) |
| 3751 | return; |
| 3752 | |
| 3753 | // FIXME: This likely won't work with graphemes. |
| 3754 | String transposed = makeString(text[1], text[0]); |
| 3755 | |
| 3756 | // Select the two characters. |
| 3757 | if (newSelection != document->selection().selection()) { |
| 3758 | if (!document->selection().shouldChangeSelection(newSelection)) |
| 3759 | return; |
| 3760 | document->selection().setSelection(newSelection); |
| 3761 | } |
| 3762 | |
| 3763 | // Insert the transposed characters. |
| 3764 | if (!shouldInsertText(transposed, newSelection.firstRange(), EditorInsertAction::Typed)) |
| 3765 | return; |
| 3766 | replaceSelectionWithText(transposed, SelectReplacement::No, SmartReplace::No, EditAction::Insert); |
| 3767 | } |
| 3768 | |
| 3769 | void Editor::addRangeToKillRing(const SimpleRange& range, KillRingInsertionMode mode) |
| 3770 | { |
| 3771 | addTextToKillRing(plainText(range), mode); |
| 3772 | } |
| 3773 | |
| 3774 | void Editor::addTextToKillRing(const String& text, KillRingInsertionMode mode) |
| 3775 | { |
| 3776 | if (m_shouldStartNewKillRingSequence) |
| 3777 | killRing().startNewSequence(); |
| 3778 | |
| 3779 | m_shouldStartNewKillRingSequence = false; |
| 3780 | |
| 3781 | // If the kill was from a backwards motion, prepend to the kill ring. |
| 3782 | // This will ensure that alternating forward and backward kills will |
| 3783 | // build up the original string in the kill ring without permuting it. |
| 3784 | switch (mode) { |
| 3785 | case KillRingInsertionMode::PrependText: |
| 3786 | killRing().prepend(text); |
| 3787 | break; |
| 3788 | case KillRingInsertionMode::AppendText: |
| 3789 | killRing().append(text); |
| 3790 | break; |
| 3791 | } |
| 3792 | } |
| 3793 | |
| 3794 | void Editor::startAlternativeTextUITimer() |
| 3795 | { |
| 3796 | m_alternativeTextController->startAlternativeTextUITimer(AlternativeTextType::Correction); |
| 3797 | } |
| 3798 | |
| 3799 | void Editor::handleAlternativeTextUIResult(const String& correction) |
| 3800 | { |
| 3801 | m_alternativeTextController->handleAlternativeTextUIResult(correction); |
| 3802 | } |
| 3803 | |
| 3804 | |
| 3805 | void Editor::dismissCorrectionPanelAsIgnored() |
| 3806 | { |
| 3807 | m_alternativeTextController->dismiss(ReasonForDismissingAlternativeText::Ignored); |
| 3808 | } |
| 3809 | |
| 3810 | void Editor::changeSelectionAfterCommand(const VisibleSelection& newSelection, OptionSet<FrameSelection::SetSelectionOption> options) |
| 3811 | { |
| 3812 | Ref document = this->document(); |
| 3813 | |
| 3814 | if (newSelection.isOrphan() || newSelection.document() != document.ptr()) |
| 3815 | return; |
| 3816 | |
| 3817 | // If there is no selection change, don't bother sending shouldChangeSelection, but still call setSelection, |
| 3818 | // because there is work that it must do in this situation. |
| 3819 | // The old selection can be invalid here and calling shouldChangeSelection can produce some strange calls. |
| 3820 | // See <rdar://problem/5729315> Some shouldChangeSelectedDOMRange contain Ranges for selections that are no longer valid |
| 3821 | bool selectionDidNotChangeDOMPosition = newSelection == document->selection().selection(); |
| 3822 | if (selectionDidNotChangeDOMPosition || document->selection().shouldChangeSelection(newSelection)) |
| 3823 | document->selection().setSelection(newSelection, options); |
| 3824 | |
| 3825 | ASSERT_WITH_SECURITY_IMPLICATION(!document->selection().selection().isOrphan())((void)0); |
| 3826 | |
| 3827 | // Some editing operations change the selection visually without affecting its position within the DOM. |
| 3828 | // For example when you press return in the following (the caret is marked by ^): |
| 3829 | // <div contentEditable="true"><div>^Hello</div></div> |
| 3830 | // WebCore inserts <div><br></div> *before* the current block, which correctly moves the paragraph down but which doesn't |
| 3831 | // change the caret's DOM position (["hello", 0]). In these situations the above FrameSelection::setSelection call |
| 3832 | // does not call EditorClient::respondToChangedSelection(), which, on the Mac, sends selection change notifications and |
| 3833 | // starts a new kill ring sequence, but we want to do these things (matches AppKit). |
| 3834 | #if PLATFORM(IOS_FAMILY)(defined 1 && 1) |
| 3835 | // FIXME: Should suppress selection change notifications during a composition change <https://webkit.org/b/38830> |
| 3836 | if (m_ignoreSelectionChanges) |
| 3837 | return; |
| 3838 | #endif |
| 3839 | if (selectionDidNotChangeDOMPosition && client()) |
| 3840 | client()->respondToChangedSelection(document->frame()); |
| 3841 | } |
| 3842 | |
| 3843 | String Editor::selectedText() const |
| 3844 | { |
| 3845 | auto options = OptionSet { TextIteratorBehavior::TraversesFlatTree }; |
| 3846 | if (!document().quirks().needsToCopyUserSelectNoneQuirk()) |
| 3847 | options.add(TextIteratorBehavior::IgnoresUserSelectNone); |
| 3848 | return selectedText(options); |
| 3849 | } |
| 3850 | |
| 3851 | String Editor::selectedTextForDataTransfer() const |
| 3852 | { |
| 3853 | auto options = OptionSet { TextIteratorBehavior::EmitsImageAltText, TextIteratorBehavior::TraversesFlatTree }; |
| 3854 | if (!document().quirks().needsToCopyUserSelectNoneQuirk()) |
| 3855 | options.add(TextIteratorBehavior::IgnoresUserSelectNone); |
| 3856 | return selectedText(options); |
| 3857 | } |
| 3858 | |
| 3859 | String Editor::selectedText(TextIteratorBehaviors behaviors) const |
| 3860 | { |
| 3861 | // We remove '\0' characters because they are not visibly rendered to the user. |
| 3862 | auto range = document().selection().selection().firstRange(); |
| 3863 | return range ? makeStringByReplacingAll(plainText(*range, behaviors), '\0', ""_s) : emptyString(); |
| 3864 | } |
| 3865 | |
| 3866 | RefPtr<TextPlaceholderElement> Editor::insertTextPlaceholder(const IntSize& size) |
| 3867 | { |
| 3868 | Ref document = this->document(); |
| 3869 | if (document->selection().isNone() || !document->selection().selection().isContentEditable()) |
| 3870 | return nullptr; |
| 3871 | |
| 3872 | // FIXME: Write in terms of replaceSelectionWithFragment(). See <https://bugs.webkit.org/show_bug.cgi?id=208744>. |
| 3873 | deleteSelectionWithSmartDelete(false); |
| 3874 | |
| 3875 | auto range = document->selection().selection().toNormalizedRange(); |
| 3876 | if (!range) |
| 3877 | return nullptr; |
| 3878 | |
| 3879 | auto placeholder = TextPlaceholderElement::create(document, size); |
| 3880 | createLiveRange(*range)->insertNode(placeholder.copyRef()); |
| 3881 | |
| 3882 | // Inserting the placeholder can run arbitrary JavaScript. Check that it still has a parent. |
| 3883 | if (!placeholder->parentNode()) |
| 3884 | return nullptr; |
| 3885 | |
| 3886 | document->selection().addCaretVisibilitySuppressionReason(CaretVisibilitySuppressionReason::TextPlaceholderIsShowing); |
| 3887 | |
| 3888 | document->selection().setSelection(VisibleSelection { positionInParentBeforeNode(placeholder) }, FrameSelection::defaultSetSelectionOptions(UserTriggered::Yes)); |
| 3889 | |
| 3890 | #if ENABLE(WRITING_TOOLS)(defined 1 && 1) |
| 3891 | // For Writing Tools, we need the snapshot of the last inserted placeholder. |
| 3892 | if (auto placeholderRange = makeRangeSelectingNode(placeholder.get())) |
| 3893 | document->page()->chrome().client().saveSnapshotOfTextPlaceholderForAnimation(*placeholderRange); |
| 3894 | #endif |
| 3895 | |
| 3896 | return placeholder; |
| 3897 | } |
| 3898 | |
| 3899 | void Editor::removeTextPlaceholder(TextPlaceholderElement& placeholder) |
| 3900 | { |
| 3901 | ASSERT(placeholder.isConnected())((void)0); |
| 3902 | |
| 3903 | Ref document = this->document(); |
| 3904 | |
| 3905 | // Save off state so that we can set the text insertion position to just before the placeholder element after removal. |
| 3906 | RefPtr savedRootEditableElement { placeholder.rootEditableElement() }; |
| 3907 | auto savedPositionBeforePlaceholder = positionInParentBeforeNode(placeholder); |
| 3908 | |
| 3909 | // FIXME: Save the current selection if it has changed since the placeholder was inserted |
| 3910 | // and restore it after text insertion. |
| 3911 | placeholder.remove(); |
| 3912 | // To match the Legacy WebKit implementation, set the text insertion point to be before where the placeholder used to be. |
| 3913 | if (document->selection().isFocusedAndActive() && document->focusedElement() == savedRootEditableElement) |
| 3914 | document->selection().setSelection(VisibleSelection { savedPositionBeforePlaceholder }, FrameSelection::defaultSetSelectionOptions(UserTriggered::Yes)); |
| 3915 | |
| 3916 | document->selection().removeCaretVisibilitySuppressionReason(CaretVisibilitySuppressionReason::TextPlaceholderIsShowing); |
| 3917 | } |
| 3918 | |
| 3919 | static inline void collapseCaretWidth(IntRect& rect) |
| 3920 | { |
| 3921 | // FIXME: Width adjustment doesn't work for rotated text. |
| 3922 | if (rect.width() == caretWidth()) |
| 3923 | rect.setWidth(0); |
| 3924 | else if (rect.height() == caretWidth()) |
| 3925 | rect.setHeight(0); |
| 3926 | } |
| 3927 | |
| 3928 | IntRect Editor::firstRectForRange(const SimpleRange& range) const |
| 3929 | { |
| 3930 | protect(range.start.document())->updateLayout(); |
| 3931 | |
| 3932 | VisiblePosition start(makeDeprecatedLegacyPosition(range.start)); |
| 3933 | |
| 3934 | if (range.collapsed()) { |
| 3935 | // FIXME: Getting caret rect and removing caret width is a very roundabout way to get collapsed range location. |
| 3936 | // In particular, width adjustment doesn't work for rotated text. |
| 3937 | IntRect startCaretRect = RenderedPosition(start).absoluteRect(); |
| 3938 | collapseCaretWidth(startCaretRect); |
| 3939 | return startCaretRect; |
| 3940 | } |
| 3941 | |
| 3942 | VisiblePosition end(makeDeprecatedLegacyPosition(range.end), Affinity::Upstream); |
| 3943 | |
| 3944 | if (inSameLine(start, end)) |
| 3945 | return enclosingIntRect(unitedBoundingBoxes(RenderObject::absoluteTextQuads(range))); |
| 3946 | |
| 3947 | return RenderedPosition(start).absoluteRect(CaretRectMode::ExpandToEndOfLine); |
| 3948 | } |
| 3949 | |
| 3950 | bool Editor::shouldChangeSelection(const VisibleSelection& oldSelection, const VisibleSelection& newSelection, Affinity affinity, bool stillSelecting) const |
| 3951 | { |
| 3952 | #if PLATFORM(IOS_FAMILY)(defined 1 && 1) |
| 3953 | if (document().frame() && document().frame()->selectionChangeCallbacksDisabled()) |
| 3954 | return true; |
| 3955 | #endif |
| 3956 | return client() && client()->shouldChangeSelectedRange(oldSelection.toNormalizedRange(), newSelection.toNormalizedRange(), affinity, stillSelecting); |
| 3957 | } |
| 3958 | |
| 3959 | void Editor::computeAndSetTypingStyle(EditingStyle& style, EditAction editingAction) |
| 3960 | { |
| 3961 | Ref document = this->document(); |
| 3962 | if (style.isEmpty()) { |
| 3963 | document->selection().clearTypingStyle(); |
| 3964 | return; |
| 3965 | } |
| 3966 | |
| 3967 | // Calculate the current typing style. |
| 3968 | RefPtr<EditingStyle> typingStyle; |
| 3969 | if (RefPtr existingTypingStyle = document->selection().typingStyle()) |
| 3970 | typingStyle = existingTypingStyle->copy(); |
| 3971 | else |
| 3972 | typingStyle = EditingStyle::create(); |
| 3973 | typingStyle->overrideTypingStyleAt(style, document->selection().selection().visibleStart().deepEquivalent()); |
| 3974 | |
| 3975 | // Handle block styles, substracting these from the typing style. |
| 3976 | RefPtr<EditingStyle> blockStyle = typingStyle->extractAndRemoveBlockProperties(); |
| 3977 | if (!blockStyle->isEmpty()) |
| 3978 | ApplyStyleCommand::create(document.copyRef(), blockStyle.get(), editingAction)->apply(); |
| 3979 | |
| 3980 | // Set the remaining style as the typing style. |
| 3981 | document->selection().setTypingStyle(WTF::move(typingStyle)); |
| 3982 | } |
| 3983 | |
| 3984 | void Editor::computeAndSetTypingStyle(StyleProperties& properties, EditAction editingAction) |
| 3985 | { |
| 3986 | return computeAndSetTypingStyle(EditingStyle::create(&properties), editingAction); |
| 3987 | } |
| 3988 | |
| 3989 | void Editor::textFieldDidBeginEditing(Element& e) |
| 3990 | { |
| 3991 | if (client()) |
| 3992 | client()->textFieldDidBeginEditing(e); |
| 3993 | } |
| 3994 | |
| 3995 | void Editor::textFieldDidEndEditing(Element& e) |
| 3996 | { |
| 3997 | dismissCorrectionPanelAsIgnored(); |
| 3998 | if (client()) |
| 3999 | client()->textFieldDidEndEditing(e); |
| 4000 | } |
| 4001 | |
| 4002 | void Editor::textDidChangeInTextField(Element& e) |
| 4003 | { |
| 4004 | if (client()) |
| 4005 | client()->textDidChangeInTextField(e); |
| 4006 | } |
| 4007 | |
| 4008 | bool Editor::doTextFieldCommandFromEvent(Element& e, KeyboardEvent* ke) |
| 4009 | { |
| 4010 | if (client()) |
| 4011 | return client()->doTextFieldCommandFromEvent(e, ke); |
| 4012 | |
| 4013 | return false; |
| 4014 | } |
| 4015 | |
| 4016 | void Editor::textWillBeDeletedInTextField(Element& input) |
| 4017 | { |
| 4018 | if (client()) |
| 4019 | client()->textWillBeDeletedInTextField(input); |
| 4020 | } |
| 4021 | |
| 4022 | void Editor::textDidChangeInTextArea(Element& e) |
| 4023 | { |
| 4024 | if (client()) |
| 4025 | client()->textDidChangeInTextArea(e); |
| 4026 | } |
| 4027 | |
| 4028 | void Editor::applyEditingStyleToBodyElement() const |
| 4029 | { |
| 4030 | RefPtr body { document().body() }; |
| 4031 | if (!body) |
| 4032 | return; |
| 4033 | body->setInlineStyleProperty(CSSPropertyOverflowWrap, CSSValueBreakWord); |
| 4034 | body->setInlineStyleProperty(CSSPropertyWebkitNbspMode, CSSValueSpace); |
| 4035 | body->setInlineStyleProperty(CSSPropertyLineBreak, CSSValueAfterWhiteSpace); |
| 4036 | } |
| 4037 | |
| 4038 | std::optional<SimpleRange> Editor::findString(const String& target, FindOptions options) |
| 4039 | { |
| 4040 | Ref document = this->document(); |
| 4041 | std::optional<SimpleRange> resultRange; |
| 4042 | { |
| 4043 | document->updateLayoutIgnorePendingStylesheets({ LayoutOptions::TreatContentVisibilityAutoAsVisible, LayoutOptions::TreatRevealedWhenFoundAsVisible }); |
| 4044 | Style::PostResolutionCallbackDisabler disabler(document); |
| 4045 | VisibleSelection selection = document->selection().selection(); |
| 4046 | auto referenceRange = selection.firstRange(); |
| 4047 | if (!referenceRange || referenceRange->collapsed()) |
| 4048 | referenceRange = selection.range(); |
| 4049 | resultRange = rangeOfString(target, referenceRange, options); |
| 4050 | } |
| 4051 | |
| 4052 | if (!resultRange) |
| 4053 | return std::nullopt; |
| 4054 | |
| 4055 | if (!options.contains(FindOption::DoNotSetSelection)) |
| 4056 | document->selection().setSelection(VisibleSelection(*resultRange)); |
| 4057 | |
| 4058 | if (!(options.contains(FindOption::DoNotRevealSelection))) |
| 4059 | document->selection().revealSelection(); |
| 4060 | |
| 4061 | return resultRange; |
| 4062 | } |
| 4063 | |
| 4064 | template<typename T> static auto& start(T& range, FindOptions options) |
| 4065 | { |
| 4066 | return options.contains(FindOption::Backwards) ? range.end : range.start; |
| 4067 | } |
| 4068 | |
| 4069 | template<typename T> static auto& end(T& range, FindOptions options) |
| 4070 | { |
| 4071 | return options.contains(FindOption::Backwards) ? range.start : range.end; |
| 4072 | } |
| 4073 | |
| 4074 | static BoundaryPoint makeBoundaryPointAfterNodeContents(Node& node, FindOptions options) |
| 4075 | { |
| 4076 | return options.contains(FindOption::Backwards) ? makeBoundaryPointBeforeNodeContents(node) : makeBoundaryPointAfterNodeContents(node); |
| 4077 | } |
| 4078 | |
| 4079 | static std::optional<BoundaryPoint> makeBoundaryPointAfterNode(Node& node, FindOptions options) |
| 4080 | { |
| 4081 | return options.contains(FindOption::Backwards) ? makeBoundaryPointBeforeNode(node) : makeBoundaryPointAfterNode(node); |
| 4082 | } |
| 4083 | |
| 4084 | static SimpleRange collapseIfRootsDiffer(SimpleRange&& range) |
| 4085 | { |
| 4086 | // FIXME: This helps correct results in some cases involving shadow trees. But we can incorrectly find a string with middle characters in an input element and first and last characters outside it. |
| 4087 | return &range.start.container->rootNode() == &range.end.container->rootNode() |
| 4088 | ? WTF::move(range) : SimpleRange { range.start, range.start }; |
| 4089 | } |
| 4090 | |
| 4091 | std::optional<SimpleRange> Editor::rangeOfString(const String& target, const std::optional<SimpleRange>& referenceRange, FindOptions options) |
| 4092 | { |
| 4093 | if (target.isEmpty()) |
| 4094 | return std::nullopt; |
| 4095 | |
| 4096 | // Start from an edge of the reference range, if there's a reference range that's not in shadow content. Which edge |
| 4097 | // is used depends on whether we're searching forward or backward, and whether startInSelection is set. |
| 4098 | |
| 4099 | bool startInReferenceRange = referenceRange && options.contains(FindOption::StartInSelection); |
| 4100 | RefPtr shadowTreeRoot = referenceRange ? referenceRange->startContainer().containingShadowRoot() : nullptr; |
| 4101 | |
| 4102 | Ref document = this->document(); |
| 4103 | auto searchRange = makeRangeSelectingNodeContents(document); |
| 4104 | if (referenceRange) |
| 4105 | start(searchRange, options) = startInReferenceRange ? start(*referenceRange, options) : end(*referenceRange, options); |
| 4106 | if (shadowTreeRoot) |
| 4107 | end(searchRange, options) = makeBoundaryPointAfterNodeContents(*shadowTreeRoot, options); |
| 4108 | auto resultRange = collapseIfRootsDiffer(findPlainText(searchRange, target, options)); |
| 4109 | |
| 4110 | // If we started in the reference range and the found range exactly matches the reference range, find again. |
| 4111 | // Build a selection with the found range to remove collapsed whitespace. |
| 4112 | // Compare ranges instead of selection objects to ignore the way that the current selection was made. |
| 4113 | auto resultSelection = VisibleSelection(resultRange); |
| 4114 | auto normalizedRange = resultSelection.toNormalizedRange(); |
| 4115 | auto resultRangeForComparison = !normalizedRange || normalizedRange->collapsed() ? resultSelection.range() : normalizedRange; |
| 4116 | if (startInReferenceRange && resultRangeForComparison == referenceRange) { |
| 4117 | searchRange = makeRangeSelectingNodeContents(document); |
| 4118 | start(searchRange, options) = end(*referenceRange, options); |
| 4119 | if (shadowTreeRoot) |
| 4120 | end(searchRange, options) = makeBoundaryPointAfterNodeContents(*shadowTreeRoot, options); |
| 4121 | resultRange = collapseIfRootsDiffer(findPlainText(searchRange, target, options)); |
| 4122 | } |
| 4123 | |
| 4124 | // If nothing was found in the shadow tree, search in main content following the shadow tree. |
| 4125 | if (resultRange.collapsed() && shadowTreeRoot) { |
| 4126 | searchRange = makeRangeSelectingNodeContents(document); |
| 4127 | if (RefPtr host = shadowTreeRoot->shadowHost()) |
| 4128 | start(searchRange, options) = *makeBoundaryPointAfterNode(*host, options); |
| 4129 | resultRange = collapseIfRootsDiffer(findPlainText(searchRange, target, options)); |
| 4130 | } |
| 4131 | |
| 4132 | // If we didn't find anything and we're wrapping, search again in the entire document (this will |
| 4133 | // redundantly re-search the area already searched in some cases). |
| 4134 | if (resultRange.collapsed() && options.contains(FindOption::WrapAround)) { |
| 4135 | resultRange = collapseIfRootsDiffer(findPlainText(makeRangeSelectingNodeContents(document), target, options)); |
| 4136 | // We used to return false here if we ended up with the same range that we started with |
| 4137 | // (e.g., the reference range was already the only instance of this text). But we decided that |
| 4138 | // this should be a success case instead, so we'll just fall through in that case. |
| 4139 | } |
| 4140 | |
| 4141 | return resultRange.collapsed() ? std::nullopt : std::make_optional(resultRange); |
| 4142 | } |
| 4143 | |
| 4144 | static bool isFrameInRange(LocalFrame& frame, const SimpleRange& range) |
| 4145 | { |
| 4146 | for (RefPtr ownerElement = frame.ownerElement(); ownerElement; ownerElement = ownerElement->document().ownerElement()) { |
| 4147 | if (&ownerElement->document() == &range.start.document()) |
| 4148 | return intersects<ComposedTree>(range, *ownerElement); |
| 4149 | } |
| 4150 | return false; |
| 4151 | } |
| 4152 | |
| 4153 | unsigned Editor::countMatchesForText(const String& target, const std::optional<SimpleRange>& range, FindOptions options, unsigned limit, bool markMatches, Vector<SimpleRange>* matches) |
| 4154 | { |
| 4155 | if (target.isEmpty()) |
| 4156 | return 0; |
| 4157 | |
| 4158 | Ref document = this->document(); |
| 4159 | document->updateLayoutIgnorePendingStylesheets({ LayoutOptions::TreatContentVisibilityAutoAsVisible, LayoutOptions::TreatRevealedWhenFoundAsVisible }); |
| 4160 | |
| 4161 | std::optional<SimpleRange> searchRange; |
| 4162 | if (range) { |
| 4163 | if (&range->start.document() == document.ptr()) |
| 4164 | searchRange = *range; |
| 4165 | else if (!isFrameInRange(*document->frame(), *range)) |
| 4166 | return 0; |
| 4167 | } |
| 4168 | if (!searchRange) |
| 4169 | searchRange = makeRangeSelectingNodeContents(document); |
| 4170 | |
| 4171 | auto allMatches = findAllPlainText(*searchRange, target, options - FindOption::Backwards, limit); |
| 4172 | |
| 4173 | if (matches) |
| 4174 | matches->appendVector(allMatches); |
| 4175 | |
| 4176 | if (markMatches) { |
| 4177 | for (const auto& match : allMatches) |
| 4178 | addMarker(match, DocumentMarkerType::TextMatch); |
| 4179 | } |
| 4180 | |
| 4181 | return allMatches.size(); |
| 4182 | } |
| 4183 | |
| 4184 | void Editor::setMarkedTextMatchesAreHighlighted(bool flag) |
| 4185 | { |
| 4186 | if (flag == m_areMarkedTextMatchesHighlighted) |
| 4187 | return; |
| 4188 | |
| 4189 | m_areMarkedTextMatchesHighlighted = flag; |
| 4190 | if (CheckedPtr markers = document().markersIfExists()) |
| 4191 | markers->repaintMarkers(DocumentMarkerType::TextMatch); |
| 4192 | } |
| 4193 | |
| 4194 | #if !PLATFORM(MAC)(defined WTF_PLATFORM_MAC && WTF_PLATFORM_MAC) |
| 4195 | void Editor::selectionWillChange() |
| 4196 | { |
| 4197 | } |
| 4198 | #endif |
| 4199 | |
| 4200 | void Editor::respondToChangedSelection(const VisibleSelection&, OptionSet<FrameSelection::SetSelectionOption> options) |
| 4201 | { |
| 4202 | #if PLATFORM(IOS_FAMILY)(defined 1 && 1) |
| 4203 | // FIXME: Should suppress selection change notifications during a composition change <https://webkit.org/b/38830> |
| 4204 | if (m_ignoreSelectionChanges) |
| 4205 | return; |
| 4206 | #endif |
| 4207 | |
| 4208 | Ref document = this->document(); |
| 4209 | |
| 4210 | #if PLATFORM(IOS_FAMILY)(defined 1 && 1) |
| 4211 | auto continueDisplayingSuggestion = [&] { |
| 4212 | if (!m_writingSuggestionData) |
| 4213 | return false; |
| 4214 | |
| 4215 | auto newSelection = document->selection().selection(); |
| 4216 | |
| 4217 | auto range = newSelection.firstRange(); |
| 4218 | if (!range) |
| 4219 | return false; |
| 4220 | |
| 4221 | range->start.offset = 0; |
| 4222 | |
| 4223 | auto completion = makeString(m_writingSuggestionData->originalPrefix(), m_writingSuggestionData->originalSuffix()); |
| 4224 | auto content = plainTextReplacingNoBreakSpace(*range); |
| 4225 | |
| 4226 | auto currentFullText = makeString(content, m_writingSuggestionData->content()); |
| 4227 | |
| 4228 | if (completion == content) |
| 4229 | return false; |
| 4230 | |
| 4231 | if (completion != currentFullText) |
| 4232 | return false; |
| 4233 | |
| 4234 | if (content.length() <= m_writingSuggestionData->originalPrefix().length()) |
| 4235 | return false; |
| 4236 | |
| 4237 | return completion.startsWith(content); |
| 4238 | }(); |
| 4239 | |
| 4240 | if (m_writingSuggestionData && !continueDisplayingSuggestion) |
| 4241 | removeWritingSuggestionIfNeeded(); |
| 4242 | #endif |
| 4243 | |
| 4244 | if (client()) |
| 4245 | client()->respondToChangedSelection(document->frame()); |
| 4246 | |
| 4247 | #if ENABLE(TELEPHONE_NUMBER_DETECTION)(defined 1 && 1) && !PLATFORM(IOS_FAMILY)(defined 1 && 1) |
| 4248 | if (shouldDetectTelephoneNumbers()) |
| 4249 | m_telephoneNumberDetectionUpdateTimer.restart(); |
| 4250 | #endif |
| 4251 | |
| 4252 | setStartNewKillRingSequence(true); |
| 4253 | m_imageElementsToLoadBeforeRevealingSelection.clear(); |
| 4254 | |
| 4255 | if (!m_hasHandledAnyEditing && !document->hasHadUserInteraction() && !document->isTopDocument()) |
| 4256 | return; |
| 4257 | |
| 4258 | if (m_editorUIUpdateTimer.isActive()) |
| 4259 | return; |
| 4260 | |
| 4261 | // Don't check spelling and grammar if the change of selection is triggered by spelling correction itself. |
| 4262 | m_editorUIUpdateTimerShouldCheckSpellingAndGrammar = options.contains(FrameSelection::SetSelectionOption::CloseTyping) && !options.contains(FrameSelection::SetSelectionOption::SpellCorrectionTriggered); |
| 4263 | m_editorUIUpdateTimerWasTriggeredByDictation = options.contains(FrameSelection::SetSelectionOption::DictationTriggered); |
| 4264 | scheduleEditorUIUpdate(); |
| 4265 | } |
| 4266 | |
| 4267 | #if ENABLE(TELEPHONE_NUMBER_DETECTION)(defined 1 && 1) && PLATFORM(MAC)(defined WTF_PLATFORM_MAC && WTF_PLATFORM_MAC) |
| 4268 | |
| 4269 | bool Editor::shouldDetectTelephoneNumbers() const |
| 4270 | { |
| 4271 | return document().isTelephoneNumberParsingEnabled() && TelephoneNumberDetector::isSupported(); |
| 4272 | } |
| 4273 | |
| 4274 | static Vector<SimpleRange> scanForTelephoneNumbers(const SimpleRange& range) |
| 4275 | { |
| 4276 | // Don't scan for phone numbers inside editable regions. |
| 4277 | if (Ref startNode = range.startContainer(); startNode->hasEditableStyle()) |
| 4278 | return { }; |
| 4279 | |
| 4280 | if (ImageOverlay::isInsideOverlay(range)) |
| 4281 | return { }; |
| 4282 | |
| 4283 | auto text = plainText(range); |
| 4284 | Vector<SimpleRange> result; |
| 4285 | int relativeStartPosition = 0; |
| 4286 | int relativeEndPosition = 0; |
| 4287 | auto characters = StringView { text }.upconvertedCharacters(); |
| 4288 | auto span = characters.span(); |
| 4289 | while (!span.empty() && TelephoneNumberDetector::find(span, &relativeStartPosition, &relativeEndPosition)) { |
| 4290 | auto scannerPosition = span.data() - characters.span().data(); |
| 4291 | ASSERT(scannerPosition + relativeEndPosition <= text.length())((void)0); |
| 4292 | result.append(resolveCharacterRange(range, CharacterRange(scannerPosition + relativeStartPosition, relativeEndPosition - relativeStartPosition))); |
| 4293 | skip(span, relativeEndPosition); |
| 4294 | } |
| 4295 | return result; |
| 4296 | } |
| 4297 | |
| 4298 | static std::optional<SimpleRange> extendSelection(const SimpleRange& range, unsigned charactersToExtend) |
| 4299 | { |
| 4300 | auto start = makeDeprecatedLegacyPosition(range.start); |
| 4301 | auto end = makeDeprecatedLegacyPosition(range.end); |
| 4302 | for (unsigned i = 0; i < charactersToExtend; ++i) { |
| 4303 | start = start.previous(Character); |
| 4304 | end = end.next(Character); |
| 4305 | } |
| 4306 | return makeSimpleRange(start, end); |
| 4307 | } |
| 4308 | |
| 4309 | void Editor::scanSelectionForTelephoneNumbers() |
| 4310 | { |
| 4311 | // FIXME: Why is it helpful here to check client for null? |
| 4312 | if (!shouldDetectTelephoneNumbers() || !client()) |
| 4313 | return; |
| 4314 | |
| 4315 | m_detectedTelephoneNumberRanges.clear(); |
| 4316 | |
| 4317 | auto notifyController = makeScopeExit([&] { |
| 4318 | if (RefPtr page = document().page()) |
| 4319 | protect(page->servicesOverlayController())->selectedTelephoneNumberRangesChanged(); |
| 4320 | }); |
| 4321 | |
| 4322 | auto selection = document().selection().selection(); |
| 4323 | if (!selection.isRange()) |
| 4324 | return; |
| 4325 | |
| 4326 | auto selectedRange = selection.firstRange(); |
| 4327 | if (!selectedRange) |
| 4328 | return; |
| 4329 | |
| 4330 | // Extend the range a few characters in each direction to detect incompletely selected phone numbers. |
| 4331 | constexpr unsigned charactersToExtend = 15; |
| 4332 | auto extendedRange = extendSelection(*selectedRange, charactersToExtend); |
| 4333 | if (!extendedRange) |
| 4334 | return; |
| 4335 | |
| 4336 | for (auto& range : scanForTelephoneNumbers(*extendedRange)) { |
| 4337 | // FIXME: Why do we do this unconditionally instead of when only when it overlaps the selection? |
| 4338 | addMarker(range, DocumentMarkerType::TelephoneNumber); |
| 4339 | |
| 4340 | // Only consider ranges with a detected telephone number if they overlap with the selection. |
| 4341 | if (intersects<ComposedTree>(range, *selectedRange)) |
| 4342 | m_detectedTelephoneNumberRanges.append(range); |
| 4343 | } |
| 4344 | } |
| 4345 | |
| 4346 | #endif // ENABLE(TELEPHONE_NUMBER_DETECTION) && !PLATFORM(IOS_FAMILY) |
| 4347 | |
| 4348 | void Editor::updateEditorUINowIfScheduled() |
| 4349 | { |
| 4350 | if (!m_editorUIUpdateTimer.isActive()) |
| 4351 | return; |
| 4352 | m_editorUIUpdateTimer.stop(); |
| 4353 | editorUIUpdateTimerFired(); |
| 4354 | } |
| 4355 | |
| 4356 | void Editor::editorUIUpdateTimerFired() |
| 4357 | { |
| 4358 | VisibleSelection oldSelection = m_oldSelectionForEditorUIUpdate; |
| 4359 | |
| 4360 | m_alternativeTextController->stopPendingCorrection(oldSelection); |
| 4361 | |
| 4362 | bool isContinuousSpellCheckingEnabled = this->isContinuousSpellCheckingEnabled(); |
| 4363 | bool isContinuousGrammarCheckingEnabled = isContinuousSpellCheckingEnabled && isGrammarCheckingEnabled(); |
| 4364 | Ref document = this->document(); |
| 4365 | if (isContinuousSpellCheckingEnabled) { |
| 4366 | VisibleSelection newAdjacentWords; |
| 4367 | VisibleSelection newSelectedSentence; |
| 4368 | bool caretBrowsing = document->settings().caretBrowsingEnabled(); |
| 4369 | if (document->selection().selection().isContentEditable() || caretBrowsing) { |
| 4370 | VisiblePosition newStart(document->selection().selection().visibleStart()); |
| 4371 | #if !PLATFORM(IOS_FAMILY)(defined 1 && 1) |
| 4372 | newAdjacentWords = VisibleSelection(startOfWord(newStart, WordSide::LeftWordIfOnBoundary), endOfWord(newStart, WordSide::RightWordIfOnBoundary)); |
| 4373 | #else |
| 4374 | // If this bug gets fixed, this PLATFORM(IOS_FAMILY) code could be removed: |
| 4375 | // <rdar://problem/7259611> Word boundary code on iPhone gives different results than desktop |
| 4376 | WordSide startWordSide = WordSide::LeftWordIfOnBoundary; |
| 4377 | char32_t c = newStart.characterBefore(); |
| 4378 | // FIXME: VisiblePosition::characterAfter() and characterBefore() do not emit newlines the same |
| 4379 | // way as TextIterator, so we do an isStartOfParagraph check here. |
| 4380 | if (deprecatedIsSpaceOrNewline(c) || c == noBreakSpace || isStartOfParagraph(newStart)) { |
| 4381 | startWordSide = WordSide::RightWordIfOnBoundary; |
| 4382 | } |
| 4383 | newAdjacentWords = VisibleSelection(startOfWord(newStart, startWordSide), endOfWord(newStart, WordSide::RightWordIfOnBoundary)); |
| 4384 | #endif // !PLATFORM(IOS_FAMILY) |
| 4385 | if (isContinuousGrammarCheckingEnabled) |
| 4386 | newSelectedSentence = VisibleSelection(startOfSentence(newStart), endOfSentence(newStart)); |
| 4387 | } |
| 4388 | |
| 4389 | // When typing we check spelling elsewhere, so don't redo it here. |
| 4390 | // If this is a change in selection resulting from a delete operation, |
| 4391 | // oldSelection may no longer be in the document. |
| 4392 | if (m_editorUIUpdateTimerShouldCheckSpellingAndGrammar && oldSelection.isContentEditable() && oldSelection.start().deprecatedNode() && oldSelection.start().anchorNode()->isConnected()) { |
| 4393 | VisiblePosition oldStart(oldSelection.visibleStart()); |
| 4394 | VisibleSelection oldAdjacentWords = VisibleSelection(startOfWord(oldStart, WordSide::LeftWordIfOnBoundary), endOfWord(oldStart, WordSide::RightWordIfOnBoundary)); |
| 4395 | if (oldAdjacentWords != newAdjacentWords) { |
| 4396 | if (isContinuousGrammarCheckingEnabled) { |
| 4397 | VisibleSelection oldSelectedSentence = VisibleSelection(startOfSentence(oldStart), endOfSentence(oldStart)); |
| 4398 | markMisspellingsAndBadGrammar(oldAdjacentWords, oldSelectedSentence != newSelectedSentence, oldSelectedSentence); |
| 4399 | } else |
| 4400 | markMisspellingsAndBadGrammar(oldAdjacentWords, false, oldAdjacentWords); |
| 4401 | } |
| 4402 | } |
| 4403 | |
| 4404 | if (!textChecker() || textChecker()->shouldEraseMarkersAfterChangeSelection(TextCheckingType::Spelling)) { |
| 4405 | if (auto wordRange = newAdjacentWords.toNormalizedRange()) |
| 4406 | removeMarkers(*wordRange, DocumentMarkerType::Spelling); |
| 4407 | } |
| 4408 | if (!textChecker() || textChecker()->shouldEraseMarkersAfterChangeSelection(TextCheckingType::Grammar)) { |
| 4409 | if (auto sentenceRange = newSelectedSentence.toNormalizedRange()) |
| 4410 | removeMarkers(*sentenceRange, DocumentMarkerType::Grammar); |
| 4411 | } |
| 4412 | } |
| 4413 | |
| 4414 | // When continuous spell checking is off, existing markers disappear after the selection changes. |
| 4415 | if (CheckedPtr markers = document->markersIfExists()) { |
| 4416 | if (!isContinuousSpellCheckingEnabled) |
| 4417 | markers->removeMarkers(DocumentMarkerType::Spelling); |
| 4418 | if (!isContinuousGrammarCheckingEnabled) |
| 4419 | markers->removeMarkers(DocumentMarkerType::Grammar); |
| 4420 | } |
| 4421 | |
| 4422 | if (!m_editorUIUpdateTimerWasTriggeredByDictation) |
| 4423 | m_alternativeTextController->respondToChangedSelection(oldSelection); |
| 4424 | |
| 4425 | m_oldSelectionForEditorUIUpdate = document->selection().selection(); |
| 4426 | |
| 4427 | #if ENABLE(ATTACHMENT_ELEMENT)(defined 1 && 1) |
| 4428 | notifyClientOfAttachmentUpdates(); |
| 4429 | #endif |
| 4430 | } |
| 4431 | |
| 4432 | static RefPtr<Node> findFirstMarkable(Node* startingNode) |
| 4433 | { |
| 4434 | RefPtr node { startingNode }; |
| 4435 | while (node) { |
| 4436 | { |
| 4437 | ScriptDisallowedScope::InMainThread scriptDisallowedScope; |
| 4438 | if (!node->renderer()) |
| 4439 | return nullptr; |
| 4440 | if (node->renderer()->isRenderTextOrLineBreak()) |
| 4441 | return node; |
| 4442 | } |
| 4443 | if (RefPtr element = dynamicDowncast<HTMLTextFormControlElement>(*node); element && element->isTextField()) |
| 4444 | node = element->visiblePositionForIndex(1).deepEquivalent().deprecatedNode(); |
| 4445 | else if (node->firstChild()) |
| 4446 | node = node->firstChild(); |
| 4447 | else |
| 4448 | node = node->nextSibling(); |
| 4449 | } |
| 4450 | |
| 4451 | return nullptr; |
| 4452 | } |
| 4453 | |
| 4454 | bool Editor::selectionStartHasMarkerFor(DocumentMarkerType markerType, int from, int length) const |
| 4455 | { |
| 4456 | auto node = findFirstMarkable(protect(document().selection().selection().start().deprecatedNode()).get()); |
| 4457 | if (!node) |
| 4458 | return false; |
| 4459 | |
| 4460 | CheckedPtr markers = document().markersIfExists(); |
| 4461 | if (!markers) |
| 4462 | return false; |
| 4463 | |
| 4464 | unsigned startOffset = static_cast<unsigned>(from); |
| 4465 | unsigned endOffset = static_cast<unsigned>(from + length); |
| 4466 | for (auto& marker : markers->markersFor(*node, markerType)) { |
| 4467 | if (marker->startOffset() <= startOffset && endOffset <= marker->endOffset()) |
| 4468 | return true; |
| 4469 | } |
| 4470 | |
| 4471 | return false; |
| 4472 | } |
| 4473 | |
| 4474 | void Editor::selectionStartSetMarkerForTesting(DocumentMarkerType markerType, int from, int length, const String& data) |
| 4475 | { |
| 4476 | RefPtr node = findFirstMarkable(protect(document().selection().selection().start().deprecatedNode()).get()); |
| 4477 | if (!node) |
| 4478 | return; |
| 4479 | |
| 4480 | RefPtr text = dynamicDowncast<Text>(node); |
| 4481 | if (!text) |
| 4482 | return; |
| 4483 | |
| 4484 | CheckedRef markers = document().markers(); |
| 4485 | |
| 4486 | unsigned unsignedFrom = static_cast<unsigned>(from); |
| 4487 | unsigned unsignedLength = static_cast<unsigned>(length); |
| 4488 | |
| 4489 | switch (markerType) { |
| 4490 | case DocumentMarkerType::TransparentContent: |
| 4491 | markers->addMarker(*text, unsignedFrom, unsignedLength, markerType, DocumentMarker::TransparentContentData { node, WTF::UUID { 0 } }); |
| 4492 | return; |
| 4493 | |
| 4494 | case DocumentMarkerType::DraggedContent: |
| 4495 | markers->addMarker(*text, unsignedFrom, unsignedLength, markerType, node); |
| 4496 | return; |
| 4497 | |
| 4498 | case DocumentMarkerType::Grammar: |
| 4499 | case DocumentMarkerType::Spelling: |
| 4500 | case DocumentMarkerType::Replacement: |
| 4501 | markers->addMarker(*text, unsignedFrom, unsignedLength, markerType, data); |
| 4502 | return; |
| 4503 | |
| 4504 | default: |
| 4505 | // FIXME: Support more marker types in this testing utility function. |
| 4506 | RELEASE_ASSERT_NOT_REACHED()do { WTF::isIntegralOrPointerType(); compilerFenceForCrash(); WTFCrashWithInfo(4506, "/Volumes/Data/worker/Apple-iOS-26-Safer-CPP-Checks-EWS/build/Source/WebCore/editing/Editor.cpp" , __PRETTY_FUNCTION__ ); } while (false); |
| 4507 | } |
| 4508 | } |
| 4509 | |
| 4510 | OptionSet<TextCheckingType> Editor::resolveTextCheckingTypeMask(const Node& rootEditableElement, OptionSet<TextCheckingType> textCheckingOptions) |
| 4511 | { |
| 4512 | #if USE(AUTOMATIC_TEXT_REPLACEMENT)(defined USE_AUTOMATIC_TEXT_REPLACEMENT && USE_AUTOMATIC_TEXT_REPLACEMENT ) && !PLATFORM(IOS_FAMILY)(defined 1 && 1) |
| 4513 | bool onlyAllowsTextReplacement = false; |
| 4514 | if (RefPtr host = dynamicDowncast<HTMLInputElement>(rootEditableElement.shadowHost())) |
| 4515 | onlyAllowsTextReplacement = host->isSpellcheckDisabledExceptTextReplacement(); |
| 4516 | if (onlyAllowsTextReplacement) |
| 4517 | textCheckingOptions = textCheckingOptions & TextCheckingType::Replacement; |
| 4518 | #else |
| 4519 | UNUSED_PARAM(rootEditableElement)(void)rootEditableElement; |
| 4520 | #endif |
| 4521 | |
| 4522 | bool shouldMarkSpelling = textCheckingOptions.contains(TextCheckingType::Spelling); |
| 4523 | bool shouldMarkGrammar = textCheckingOptions.contains(TextCheckingType::Grammar); |
| 4524 | #if !PLATFORM(IOS_FAMILY)(defined 1 && 1) |
| 4525 | bool shouldShowCorrectionPanel = textCheckingOptions.contains(TextCheckingType::ShowCorrectionPanel); |
| 4526 | bool shouldCheckForCorrection = shouldShowCorrectionPanel || textCheckingOptions.contains(TextCheckingType::Correction); |
| 4527 | #else |
| 4528 | bool shouldCheckForCorrection = textCheckingOptions.contains(TextCheckingType::Correction); |
| 4529 | #endif |
| 4530 | |
| 4531 | OptionSet<TextCheckingType> checkingTypes; |
| 4532 | if (shouldMarkSpelling) |
| 4533 | checkingTypes.add(TextCheckingType::Spelling); |
| 4534 | if (shouldMarkGrammar) |
| 4535 | checkingTypes.add(TextCheckingType::Grammar); |
| 4536 | if (shouldCheckForCorrection) |
| 4537 | checkingTypes.add(TextCheckingType::Correction); |
| 4538 | #if !PLATFORM(IOS_FAMILY)(defined 1 && 1) |
| 4539 | if (shouldShowCorrectionPanel) |
| 4540 | checkingTypes.add(TextCheckingType::ShowCorrectionPanel); |
| 4541 | |
| 4542 | #if USE(AUTOMATIC_TEXT_REPLACEMENT)(defined USE_AUTOMATIC_TEXT_REPLACEMENT && USE_AUTOMATIC_TEXT_REPLACEMENT ) |
| 4543 | bool shouldPerformReplacement = textCheckingOptions.contains(TextCheckingType::Replacement); |
| 4544 | if (shouldPerformReplacement) { |
| 4545 | if (!onlyAllowsTextReplacement) { |
| 4546 | if (isAutomaticLinkDetectionEnabled()) |
| 4547 | checkingTypes.add(TextCheckingType::Link); |
| 4548 | if (isAutomaticQuoteSubstitutionEnabled()) |
| 4549 | checkingTypes.add(TextCheckingType::Quote); |
| 4550 | if (isAutomaticDashSubstitutionEnabled()) |
| 4551 | checkingTypes.add(TextCheckingType::Dash); |
| 4552 | if (shouldMarkSpelling && isAutomaticSpellingCorrectionEnabled()) |
| 4553 | checkingTypes.add(TextCheckingType::Correction); |
| 4554 | } |
| 4555 | if (isAutomaticTextReplacementEnabled()) |
| 4556 | checkingTypes.add(TextCheckingType::Replacement); |
| 4557 | } |
| 4558 | #endif |
| 4559 | #endif // !PLATFORM(IOS_FAMILY) |
| 4560 | |
| 4561 | return checkingTypes; |
| 4562 | } |
| 4563 | |
| 4564 | static bool candidateWouldReplaceText(const VisibleSelection& selection) |
| 4565 | { |
| 4566 | // If the character behind the caret in the current selection is anything but a space or a newline then we should |
| 4567 | // replace the whole current word with the candidate. |
| 4568 | char32_t characterAfterSelection, characterBeforeSelection, twoCharacterBeforeSelection = 0; |
| 4569 | charactersAroundPosition(selection.visibleStart(), characterAfterSelection, characterBeforeSelection, twoCharacterBeforeSelection); |
| 4570 | return !(characterBeforeSelection == '\0' || characterBeforeSelection == '\n' || characterBeforeSelection == ' '); |
| 4571 | } |
| 4572 | |
| 4573 | String Editor::stringForCandidateRequest() const |
| 4574 | { |
| 4575 | auto& selection = document().selection().selection(); |
| 4576 | auto range = selection.isCaret() ? wordRangeFromPosition(selection.start()) : selection.toNormalizedRange(); |
| 4577 | if (!range) |
| 4578 | return { }; |
| 4579 | if (!candidateWouldReplaceText(selection)) |
| 4580 | return { }; |
| 4581 | return plainText(*range); |
| 4582 | } |
| 4583 | |
| 4584 | std::optional<SimpleRange> Editor::contextRangeForCandidateRequest() const |
| 4585 | { |
| 4586 | auto& selection = document().selection().selection(); |
| 4587 | return makeSimpleRange(startOfParagraph(selection.visibleStart()), endOfParagraph(selection.visibleEnd())); |
| 4588 | } |
| 4589 | |
| 4590 | std::optional<SimpleRange> Editor::rangeForTextCheckingResult(const TextCheckingResult& result) const |
| 4591 | { |
| 4592 | if (!result.range.length) |
| 4593 | return std::nullopt; |
| 4594 | auto contextRange = contextRangeForCandidateRequest(); |
| 4595 | if (!contextRange) |
| 4596 | return std::nullopt; |
| 4597 | return resolveCharacterRange(*contextRange, result.range); |
| 4598 | } |
| 4599 | |
| 4600 | void Editor::scheduleEditorUIUpdate() |
| 4601 | { |
| 4602 | m_editorUIUpdateTimer.startOneShot(0_s); |
| 4603 | } |
| 4604 | |
| 4605 | #if !PLATFORM(COCOA)(defined 1 && 1) |
| 4606 | |
| 4607 | String Editor::platformContentTypeForBlobType(const String& type) const |
| 4608 | { |
| 4609 | return type; |
| 4610 | } |
| 4611 | |
| 4612 | #endif |
| 4613 | |
| 4614 | static Vector<TextList> editableTextListsAtPositionInDescendingOrder(const Position& position) |
| 4615 | { |
| 4616 | RefPtr startContainer { position.containerNode() }; |
| 4617 | if (!startContainer) |
| 4618 | return { }; |
| 4619 | |
| 4620 | auto editableRoot = highestEditableRoot(firstPositionInOrBeforeNode(startContainer.get())); |
| 4621 | if (!editableRoot) |
| 4622 | return { }; |
| 4623 | |
| 4624 | Vector<Ref<HTMLElement>> enclosingLists; |
| 4625 | for (Ref ancestor : ancestorsOfType<HTMLElement>(*startContainer)) { |
| 4626 | if (ancestor.ptr() == editableRoot.get()) |
| 4627 | break; |
| 4628 | |
| 4629 | if (!ancestor->renderer()) |
| 4630 | continue; |
| 4631 | |
| 4632 | if (isAnyOf<HTMLUListElement, HTMLOListElement>(ancestor)) |
| 4633 | enclosingLists.append(WTF::move(ancestor)); |
| 4634 | } |
| 4635 | |
| 4636 | Vector<TextList> textLists; |
| 4637 | textLists.reserveInitialCapacity(enclosingLists.size()); |
| 4638 | for (auto iterator = enclosingLists.rbegin(); iterator != enclosingLists.rend(); ++iterator) { |
| 4639 | Ref list = *iterator; |
| 4640 | RefPtr orderedList = dynamicDowncast<HTMLOListElement>(list); |
| 4641 | if (!list->renderer()) |
| 4642 | continue; |
| 4643 | auto style = list->renderer()->style().listStyleType(); |
| 4644 | textLists.append({ style, orderedList ? orderedList->start() : 1, !!orderedList }); |
| 4645 | } |
| 4646 | |
| 4647 | return textLists; |
| 4648 | } |
| 4649 | |
| 4650 | FontAttributes Editor::fontAttributesAtSelectionStart() |
| 4651 | { |
| 4652 | FontAttributes attributes; |
| 4653 | attributes.font = fontForSelection(attributes.hasMultipleFonts); |
| 4654 | |
| 4655 | RefPtr<Node> nodeToRemove; |
| 4656 | auto nodeRemovalScope = makeScopeExit([&nodeToRemove]() { |
| 4657 | if (nodeToRemove) |
| 4658 | nodeToRemove->remove(); |
| 4659 | }); |
| 4660 | |
| 4661 | CheckedPtr style = styleForSelectionStart(nodeToRemove); |
| 4662 | if (!style) |
| 4663 | return { }; |
| 4664 | |
| 4665 | ScriptDisallowedScope::InMainThread scriptDisallowedScope; |
| 4666 | |
| 4667 | // FIXME: for now, always report the colors after applying -apple-color-filter. In future not all clients |
| 4668 | // may want this, so we may have to add a setting to control it. See also editingAttributedStringFromRange(). |
| 4669 | auto backgroundColor = style->visitedDependentBackgroundColorApplyingColorFilter(); |
| 4670 | if (backgroundColor.isVisible()) |
| 4671 | attributes.backgroundColor = backgroundColor; |
| 4672 | |
| 4673 | auto foregroundColor = style->visitedDependentColorApplyingColorFilter(); |
| 4674 | // FIXME: isBlackColor not suitable for dark mode. |
| 4675 | if (foregroundColor.isValid() && !Color::isBlackColor(foregroundColor)) |
| 4676 | attributes.foregroundColor = foregroundColor; |
| 4677 | |
| 4678 | attributes.fontShadow = WTF::switchOn(style->textShadow(), |
| 4679 | [&](const CSS::Keyword::None&) { |
| 4680 | return FontShadow { }; |
| 4681 | }, |
| 4682 | [&](const auto& shadows) { |
| 4683 | Style::ColorResolver colorResolver { *style }; |
| 4684 | const auto& zoomFactor = style->usedZoomForLength(); |
| 4685 | return FontShadow { |
| 4686 | colorResolver.colorResolvingCurrentColorApplyingColorFilter(shadows[0].color), |
| 4687 | { |
| 4688 | shadows[0].location.x().resolveZoom(zoomFactor), |
| 4689 | shadows[0].location.y().resolveZoom(zoomFactor), |
| 4690 | }, |
| 4691 | shadows[0].blur.resolveZoom(zoomFactor) |
| 4692 | }; |
| 4693 | } |
| 4694 | ); |
| 4695 | |
| 4696 | attributes.subscriptOrSuperscript = WTF::switchOn(style->verticalAlign(), |
| 4697 | [](const CSS::Keyword::Baseline&) { return FontAttributes::SubscriptOrSuperscript::None; }, |
| 4698 | [](const CSS::Keyword::Sub&) { return FontAttributes::SubscriptOrSuperscript::Subscript; }, |
| 4699 | [](const CSS::Keyword::Super&) { return FontAttributes::SubscriptOrSuperscript::Superscript; }, |
| 4700 | [](const CSS::Keyword::Bottom&) { return FontAttributes::SubscriptOrSuperscript::None; }, |
| 4701 | [](const CSS::Keyword::Middle&) { return FontAttributes::SubscriptOrSuperscript::None; }, |
| 4702 | [](const CSS::Keyword::TextBottom&) { return FontAttributes::SubscriptOrSuperscript::None; }, |
| 4703 | [](const CSS::Keyword::TextTop&) { return FontAttributes::SubscriptOrSuperscript::None; }, |
| 4704 | [](const CSS::Keyword::Top&) { return FontAttributes::SubscriptOrSuperscript::None; }, |
| 4705 | [](const CSS::Keyword::WebkitBaselineMiddle&) { return FontAttributes::SubscriptOrSuperscript::None; }, |
| 4706 | [](const Style::VerticalAlign::Length&) { return FontAttributes::SubscriptOrSuperscript::None; } |
| 4707 | ); |
| 4708 | |
| 4709 | attributes.textLists = editableTextListsAtPositionInDescendingOrder(document().selection().selection().start()); |
| 4710 | |
| 4711 | switch (style->textAlign()) { |
| 4712 | case Style::TextAlign::Right: |
| 4713 | case Style::TextAlign::WebKitRight: |
| 4714 | attributes.horizontalAlignment = FontAttributes::HorizontalAlignment::Right; |
| 4715 | break; |
| 4716 | case Style::TextAlign::Left: |
| 4717 | case Style::TextAlign::WebKitLeft: |
| 4718 | attributes.horizontalAlignment = FontAttributes::HorizontalAlignment::Left; |
| 4719 | break; |
| 4720 | case Style::TextAlign::Center: |
| 4721 | case Style::TextAlign::WebKitCenter: |
| 4722 | attributes.horizontalAlignment = FontAttributes::HorizontalAlignment::Center; |
| 4723 | break; |
| 4724 | case Style::TextAlign::Justify: |
| 4725 | attributes.horizontalAlignment = FontAttributes::HorizontalAlignment::Justify; |
| 4726 | break; |
| 4727 | case Style::TextAlign::Start: |
| 4728 | if (style->hasExplicitlySetDirection()) |
| 4729 | attributes.horizontalAlignment = style->writingMode().deprecatedIsLeftToRightDirection() ? FontAttributes::HorizontalAlignment::Left : FontAttributes::HorizontalAlignment::Right; |
| 4730 | else |
| 4731 | attributes.horizontalAlignment = FontAttributes::HorizontalAlignment::Natural; |
| 4732 | break; |
| 4733 | case Style::TextAlign::End: |
| 4734 | attributes.horizontalAlignment = style->writingMode().deprecatedIsLeftToRightDirection() ? FontAttributes::HorizontalAlignment::Right : FontAttributes::HorizontalAlignment::Left; |
| 4735 | break; |
| 4736 | } |
| 4737 | |
| 4738 | RefPtr typingStyle { document().selection().typingStyle() }; |
| 4739 | if (typingStyle && typingStyle->style()) { |
| 4740 | if (RefPtr value = dynamicDowncast<CSSValueList>(typingStyle->style()->getPropertyCSSValue(CSSPropertyWebkitTextDecorationsInEffect))) { |
| 4741 | if (value->hasValue(CSSValueLineThrough)) |
| 4742 | attributes.hasStrikeThrough = true; |
| 4743 | if (value->hasValue(CSSValueUnderline)) |
| 4744 | attributes.hasUnderline = true; |
| 4745 | } |
| 4746 | } else { |
| 4747 | auto decoration = style->textDecorationLineInEffect(); |
| 4748 | if (decoration.hasLineThrough()) |
| 4749 | attributes.hasStrikeThrough = true; |
| 4750 | if (decoration.hasUnderline()) |
| 4751 | attributes.hasUnderline = true; |
| 4752 | } |
| 4753 | |
| 4754 | return attributes; |
| 4755 | } |
| 4756 | |
| 4757 | #if ENABLE(ATTACHMENT_ELEMENT)(defined 1 && 1) |
| 4758 | |
| 4759 | PromisedAttachmentInfo Editor::promisedAttachmentInfo(Element& element) |
| 4760 | { |
| 4761 | CheckedPtr client = this->client(); |
| 4762 | if (!client || !client->supportsClientSideAttachmentData()) |
| 4763 | return { }; |
| 4764 | |
| 4765 | RefPtr<HTMLAttachmentElement> attachment; |
| 4766 | if (auto* possibleAttachment = dynamicDowncast<HTMLAttachmentElement>(element)) |
| 4767 | attachment = possibleAttachment; |
| 4768 | else if (auto* image = dynamicDowncast<HTMLImageElement>(element)) |
| 4769 | attachment = image->attachmentElement(); |
| 4770 | |
| 4771 | if (!attachment) |
| 4772 | return { }; |
| 4773 | |
| 4774 | Vector<std::pair<String, RefPtr<WebCore::SharedBuffer>>> additionalTypesAndData; |
| 4775 | #if PLATFORM(COCOA)(defined 1 && 1) |
| 4776 | getPasteboardTypesAndDataForAttachment(element, additionalTypesAndData); |
| 4777 | #endif |
| 4778 | |
| 4779 | return { attachment->uniqueIdentifier(), WTF::move(additionalTypesAndData) }; |
| 4780 | } |
| 4781 | |
| 4782 | void Editor::registerAttachmentIdentifier(const String& identifier, const String& contentType, const String& preferredFileName, Ref<FragmentedSharedBuffer>&& data) |
| 4783 | { |
| 4784 | if (CheckedPtr client = this->client()) |
| 4785 | client->registerAttachmentIdentifier(identifier, contentType, preferredFileName, WTF::move(data)); |
| 4786 | } |
| 4787 | |
| 4788 | void Editor::registerAttachmentIdentifier(const String& identifier, const String& contentType, const String& filePath) |
| 4789 | { |
| 4790 | if (CheckedPtr client = this->client()) |
| 4791 | client->registerAttachmentIdentifier(identifier, contentType, filePath); |
| 4792 | } |
| 4793 | |
| 4794 | void Editor::registerAttachments(Vector<SerializedAttachmentData>&& data) |
| 4795 | { |
| 4796 | if (CheckedPtr client = this->client()) |
| 4797 | client->registerAttachments(WTF::move(data)); |
| 4798 | } |
| 4799 | |
| 4800 | void Editor::registerAttachmentIdentifier(const String& identifier, const AttachmentAssociatedElement& element) |
| 4801 | { |
| 4802 | CheckedPtr client = this->client(); |
| 4803 | if (!client) |
| 4804 | return; |
| 4805 | |
| 4806 | auto attachmentInfo = [&]() -> std::optional<std::tuple<String, String, Ref<FragmentedSharedBuffer>>> { |
| 4807 | RefPtr imageElement = dynamicDowncast<HTMLImageElement>(element.asHTMLElement()); |
| 4808 | if (!imageElement) |
| 4809 | return std::nullopt; |
| 4810 | |
| 4811 | CheckedPtr renderer = dynamicDowncast<RenderImage>(imageElement->renderer()); |
| 4812 | if (!renderer) |
| 4813 | return std::nullopt; |
| 4814 | |
| 4815 | auto* cachedImage = renderer->cachedImage(); |
Local variable 'cachedImage' is uncounted and unsafe | |
| 4816 | if (!cachedImage || cachedImage->errorOccurred()) |
| 4817 | return std::nullopt; |
| 4818 | |
| 4819 | String contentType; |
| 4820 | if (RefPtr image = cachedImage->image()) |
| 4821 | contentType = image->mimeType(); |
| 4822 | |
| 4823 | if (contentType.isEmpty()) |
| 4824 | return std::nullopt; |
| 4825 | |
| 4826 | RefPtr imageData = cachedImage->resourceBuffer(); |
| 4827 | if (!imageData) |
| 4828 | return std::nullopt; |
| 4829 | |
| 4830 | String name = imageElement->attributeWithoutSynchronization(altAttr); |
| 4831 | if (name.isEmpty()) |
| 4832 | name = imageElement->document().completeURL(imageElement->imageSourceURL()).lastPathComponent().toString(); |
| 4833 | |
| 4834 | if (name.isEmpty()) |
| 4835 | return std::nullopt; |
| 4836 | |
| 4837 | return { { WTF::move(contentType), WTF::move(name), imageData.releaseNonNull() } }; |
| 4838 | }(); |
| 4839 | |
| 4840 | if (attachmentInfo) { |
| 4841 | auto& [contentType, preferredFileName, data] = *attachmentInfo; |
| 4842 | client->registerAttachmentIdentifier(identifier, WTF::move(contentType), WTF::move(preferredFileName), WTF::move(data)); |
| 4843 | } else |
| 4844 | client->registerAttachmentIdentifier(identifier); |
| 4845 | } |
| 4846 | |
| 4847 | void Editor::cloneAttachmentData(const String& fromIdentifier, const String& toIdentifier) |
| 4848 | { |
| 4849 | if (CheckedPtr client = this->client()) |
| 4850 | client->cloneAttachmentData(fromIdentifier, toIdentifier); |
| 4851 | } |
| 4852 | |
| 4853 | void Editor::didInsertAttachmentElement(HTMLAttachmentElement& attachment) |
| 4854 | { |
| 4855 | auto identifier = attachment.uniqueIdentifier(); |
| 4856 | if (identifier.isEmpty()) |
| 4857 | return; |
| 4858 | |
| 4859 | if (!m_removedAttachmentIdentifiers.take(identifier)) |
| 4860 | m_insertedAttachmentIdentifiers.add(identifier); |
| 4861 | scheduleEditorUIUpdate(); |
| 4862 | } |
| 4863 | |
| 4864 | void Editor::didRemoveAttachmentElement(HTMLAttachmentElement& attachment) |
| 4865 | { |
| 4866 | auto identifier = attachment.uniqueIdentifier(); |
| 4867 | if (identifier.isEmpty()) |
| 4868 | return; |
| 4869 | |
| 4870 | if (!m_insertedAttachmentIdentifiers.take(identifier)) |
| 4871 | m_removedAttachmentIdentifiers.add(identifier); |
| 4872 | scheduleEditorUIUpdate(); |
| 4873 | } |
| 4874 | |
| 4875 | void Editor::notifyClientOfAttachmentUpdates() |
| 4876 | { |
| 4877 | auto removedAttachmentIdentifiers = WTF::move(m_removedAttachmentIdentifiers); |
| 4878 | auto insertedAttachmentIdentifiers = WTF::move(m_insertedAttachmentIdentifiers); |
| 4879 | if (!client()) |
| 4880 | return; |
| 4881 | |
| 4882 | for (auto& identifier : removedAttachmentIdentifiers) |
| 4883 | client()->didRemoveAttachmentWithIdentifier(identifier); |
| 4884 | |
| 4885 | for (auto& identifier : insertedAttachmentIdentifiers) { |
| 4886 | if (auto attachment = document().attachmentForIdentifier(identifier)) |
| 4887 | client()->didInsertAttachmentWithIdentifier(identifier, attachment->attributeWithoutSynchronization(HTMLNames::srcAttr), attachment->associatedElementType()); |
| 4888 | else |
| 4889 | ASSERT_NOT_REACHED()((void)0); |
| 4890 | } |
| 4891 | } |
| 4892 | |
| 4893 | void Editor::insertAttachment(const String& identifier, std::optional<uint64_t>&& fileSize, const AtomString& fileName, const AtomString& contentType) |
| 4894 | { |
| 4895 | Ref document = this->document(); |
| 4896 | auto attachment = HTMLAttachmentElement::create(HTMLNames::attachmentTag, document); |
| 4897 | attachment->setUniqueIdentifier(identifier); |
| 4898 | attachment->updateAttributes(WTF::move(fileSize), contentType, fileName); |
| 4899 | |
| 4900 | auto fragmentToInsert = document->createDocumentFragment(); |
| 4901 | fragmentToInsert->appendChild(attachment.get()); |
| 4902 | |
| 4903 | replaceSelectionWithFragment(fragmentToInsert.get(), SelectReplacement::No, SmartReplace::No, MatchStyle::Yes); |
| 4904 | } |
| 4905 | |
| 4906 | #endif // ENABLE(ATTACHMENT_ELEMENT) |
| 4907 | |
| 4908 | void Editor::handleAcceptedCandidate(TextCheckingResult acceptedCandidate) |
| 4909 | { |
| 4910 | const VisibleSelection& selection = document().selection().selection(); |
| 4911 | |
| 4912 | m_isHandlingAcceptedCandidate = true; |
| 4913 | |
| 4914 | if (auto range = rangeForTextCheckingResult(acceptedCandidate)) { |
| 4915 | if (shouldInsertText(acceptedCandidate.replacement, *range, EditorInsertAction::Typed)) |
| 4916 | ReplaceRangeWithTextCommand::create(*range, acceptedCandidate.replacement)->apply(); |
| 4917 | } else |
| 4918 | insertText(acceptedCandidate.replacement, nullptr); |
| 4919 | |
| 4920 | if (auto insertedCandidateRange = rangeExpandedByCharactersInDirectionAtWordBoundary(selection.visibleStart(), acceptedCandidate.replacement.length(), SelectionDirection::Backward)) |
| 4921 | addMarker(*insertedCandidateRange, DocumentMarkerType::AcceptedCandidate, acceptedCandidate.replacement); |
| 4922 | |
| 4923 | m_isHandlingAcceptedCandidate = false; |
| 4924 | } |
| 4925 | |
| 4926 | bool Editor::unifiedTextCheckerEnabled() const |
| 4927 | { |
| 4928 | return WebCore::unifiedTextCheckerEnabled(document().frame()); |
| 4929 | } |
| 4930 | |
| 4931 | Vector<String> Editor::dictationAlternativesForMarker(const DocumentMarker& marker) |
| 4932 | { |
| 4933 | return m_alternativeTextController->dictationAlternativesForMarker(marker); |
| 4934 | } |
| 4935 | |
| 4936 | void Editor::applyDictationAlternative(const String& alternativeString) |
| 4937 | { |
| 4938 | m_alternativeTextController->applyDictationAlternative(alternativeString); |
| 4939 | } |
| 4940 | |
| 4941 | void Editor::toggleOverwriteModeEnabled() |
| 4942 | { |
| 4943 | m_overwriteModeEnabled = !m_overwriteModeEnabled; |
| 4944 | document().selection().setShouldShowBlockCursor(m_overwriteModeEnabled); |
| 4945 | } |
| 4946 | |
| 4947 | std::optional<SimpleRange> Editor::adjustedSelectionRange() |
| 4948 | { |
| 4949 | // FIXME: Why do we need to adjust the selection to include the anchor tag it's in? Whoever wrote this code originally forgot to leave us a comment explaining the rationale. |
| 4950 | auto range = selectedRange(); |
| 4951 | if (range) { |
| 4952 | if (RefPtr enclosingAnchor = enclosingElementWithTag(firstPositionInNode(*commonInclusiveAncestor<ComposedTree>(*range)), HTMLNames::aTag)) { |
| 4953 | if (firstPositionInOrBeforeNode(range->start.container.ptr()) >= makeDeprecatedLegacyPosition(range->start)) |
| 4954 | range->start = makeBoundaryPointBeforeNodeContents(*enclosingAnchor); |
| 4955 | } |
| 4956 | } |
| 4957 | return range; |
| 4958 | } |
| 4959 | |
| 4960 | // FIXME: This figures out the current style by inserting a <span>! |
| 4961 | const RenderStyle* Editor::styleForSelectionStart(RefPtr<Node>& nodeToRemove) |
| 4962 | { |
| 4963 | nodeToRemove = nullptr; |
| 4964 | |
| 4965 | Ref document = this->document(); |
| 4966 | if (document->selection().isNone()) |
| 4967 | return nullptr; |
| 4968 | |
| 4969 | Position position = adjustedSelectionStartForStyleComputation(document->selection().selection()); |
| 4970 | if (!position.isCandidate() || position.isNull()) |
| 4971 | return nullptr; |
| 4972 | |
| 4973 | RefPtr typingStyle { document->selection().typingStyle() }; |
| 4974 | if (!typingStyle || !typingStyle->style()) |
| 4975 | return &position.deprecatedNode()->renderer()->style(); |
| 4976 | |
| 4977 | auto styleElement = HTMLSpanElement::create(document); |
| 4978 | |
| 4979 | auto styleText = makeAtomString(typingStyle->style()->asText(CSS::defaultSerializationContext()), " display: inline"_s); |
| 4980 | styleElement->setAttribute(HTMLNames::styleAttr, styleText); |
| 4981 | |
| 4982 | styleElement->appendChild(document->createEditingTextNode(String { emptyString() })); |
| 4983 | |
| 4984 | RefPtr positionNode = position.deprecatedNode(); |
| 4985 | ASSERT(positionNode)((void)0); |
| 4986 | RefPtr parent { positionNode->parentNode() }; |
| 4987 | if (!parent || parent->appendChild(styleElement.get()).hasException()) |
| 4988 | return nullptr; |
| 4989 | |
| 4990 | nodeToRemove = styleElement.ptr(); |
| 4991 | |
| 4992 | document->updateStyleIfNeeded(); |
| 4993 | return styleElement->renderer() ? &styleElement->renderer()->style() : nullptr; |
| 4994 | } |
| 4995 | |
| 4996 | RefPtr<Font> Editor::fontForSelection(bool& hasMultipleFonts) |
| 4997 | { |
| 4998 | hasMultipleFonts = false; |
| 4999 | |
| 5000 | Ref document = this->document(); |
| 5001 | if (!document->selection().isRange()) { |
| 5002 | RefPtr<Node> nodeToRemove; |
| 5003 | RefPtr<Font> font; |
| 5004 | { |
| 5005 | CheckedPtr style = styleForSelectionStart(nodeToRemove); |
| 5006 | if (!style) |
| 5007 | return nullptr; |
| 5008 | ScriptDisallowedScope::InMainThread scriptDisallowedScope; |
| 5009 | font = const_cast<Font*>(&style->fontCascade().primaryFont()); |
| 5010 | } |
| 5011 | |
| 5012 | if (nodeToRemove) |
| 5013 | nodeToRemove->remove(); |
| 5014 | |
| 5015 | return font; |
| 5016 | } |
| 5017 | |
| 5018 | auto range = document->selection().selection().toNormalizedRange(); |
| 5019 | if (!range) |
| 5020 | return nullptr; |
| 5021 | |
| 5022 | // FIXME: Adjusting the start may move it past the end. In that case the iterator below will go on to the end of the document. |
| 5023 | auto adjustedStart = makeBoundaryPoint(adjustedSelectionStartForStyleComputation(document->selection().selection())); |
| 5024 | if (!adjustedStart) |
| 5025 | return nullptr; |
| 5026 | range->start = *adjustedStart; |
| 5027 | |
| 5028 | ScriptDisallowedScope::InMainThread scriptDisallowedScope; |
| 5029 | |
| 5030 | RefPtr<Font> font; |
| 5031 | for (Ref node : intersectingNodes(*range)) { |
| 5032 | CheckedPtr renderer = node->renderer(); |
| 5033 | if (!renderer) |
| 5034 | continue; |
| 5035 | // The font of intermediate nodes that don't affect the rendering of text are not necessary to report, so limit to only such nodes. |
| 5036 | if (!node->isTextNode() && !renderer->isBR() && !TextNodeTraversal::firstChild(node)) |
| 5037 | continue; |
| 5038 | Ref primaryFont = renderer->style().fontCascade().primaryFont(); |
| 5039 | if (!font) |
| 5040 | font = const_cast<Font*>(primaryFont.ptr()); |
| 5041 | else if (font != primaryFont.ptr()) { |
| 5042 | hasMultipleFonts = true; |
| 5043 | break; |
| 5044 | } |
| 5045 | } |
| 5046 | |
| 5047 | return font; |
| 5048 | } |
| 5049 | |
| 5050 | bool Editor::canCopyExcludingStandaloneImages() const |
| 5051 | { |
| 5052 | auto& selection = document().selection().selection(); |
| 5053 | return selection.isRange() && !selection.isInPasswordField(); |
| 5054 | } |
| 5055 | |
| 5056 | } // namespace WebCore |