blob: e449bfd0000df0139880489d0ed309f1ba19f676 [file] [log] [blame]
/*
* Copyright (C) 2019 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "config.h"
#import "TextCheckingControllerProxy.h"
#if ENABLE(PLATFORM_DRIVEN_TEXT_CHECKING)
#import "ArgumentCoders.h"
#import "TextCheckingControllerProxyMessages.h"
#import "WebCoreArgumentCoders.h"
#import "WebPage.h"
#import "WebProcess.h"
#import <WebCore/DocumentMarker.h>
#import <WebCore/DocumentMarkerController.h>
#import <WebCore/Editing.h>
#import <WebCore/Editor.h>
#import <WebCore/FocusController.h>
#import <WebCore/RenderObject.h>
#import <WebCore/RenderedDocumentMarker.h>
#import <WebCore/TextIterator.h>
#import <WebCore/VisibleUnits.h>
// FIXME: Remove this after rdar://problem/48914153 is resolved.
#if PLATFORM(MACCATALYST)
typedef NS_ENUM(NSInteger, NSSpellingState) {
NSSpellingStateSpellingFlag = (1 << 0),
NSSpellingStateGrammarFlag = (1 << 1)
};
#endif
namespace WebKit {
using namespace WebCore;
TextCheckingControllerProxy::TextCheckingControllerProxy(WebPage& page)
: m_page(page)
{
WebProcess::singleton().addMessageReceiver(Messages::TextCheckingControllerProxy::messageReceiverName(), m_page.identifier(), *this);
}
TextCheckingControllerProxy::~TextCheckingControllerProxy()
{
WebProcess::singleton().removeMessageReceiver(Messages::TextCheckingControllerProxy::messageReceiverName(), m_page.identifier());
}
static OptionSet<DocumentMarker::MarkerType> relevantMarkerTypes()
{
return { DocumentMarker::PlatformTextChecking, DocumentMarker::Spelling, DocumentMarker::Grammar };
}
Optional<TextCheckingControllerProxy::RangeAndOffset> TextCheckingControllerProxy::rangeAndOffsetRelativeToSelection(int64_t offset, uint64_t length)
{
auto& frameSelection = m_page.corePage()->focusController().focusedOrMainFrame().selection();
auto& selection = frameSelection.selection();
auto root = frameSelection.rootEditableElementOrDocumentElement();
if (!root)
return WTF::nullopt;
auto selectionLiveRange = selection.toNormalizedRange();
if (!selectionLiveRange)
return WTF::nullopt;
auto selectionRange = SimpleRange { *selectionLiveRange };
auto scope = makeRangeSelectingNodeContents(*root);
int64_t adjustedStartLocation = characterCount({ scope.start, selectionRange.start }) + offset;
if (adjustedStartLocation < 0)
return WTF::nullopt;
auto adjustedSelectionCharacterRange = CharacterRange { static_cast<uint64_t>(adjustedStartLocation), length };
return { { createLiveRange(resolveCharacterRange(scope, adjustedSelectionCharacterRange)), adjustedSelectionCharacterRange.location } };
}
void TextCheckingControllerProxy::replaceRelativeToSelection(const AttributedString& annotatedString, int64_t selectionOffset, uint64_t length, uint64_t relativeReplacementLocation, uint64_t relativeReplacementLength)
{
Frame& frame = m_page.corePage()->focusController().focusedOrMainFrame();
FrameSelection& frameSelection = frame.selection();
auto root = frameSelection.rootEditableElementOrDocumentElement();
if (!root)
return;
auto rangeAndOffset = rangeAndOffsetRelativeToSelection(selectionOffset, length);
if (!rangeAndOffset)
return;
auto range = rangeAndOffset->range;
if (!range)
return;
auto locationInRoot = rangeAndOffset->locationInRoot;
auto& markers = frame.document()->markers();
markers.removeMarkers(*range, relevantMarkerTypes());
if (relativeReplacementLocation != NSNotFound) {
auto rangeAndOffsetOfReplacement = rangeAndOffsetRelativeToSelection(selectionOffset + relativeReplacementLocation, relativeReplacementLength);
if (rangeAndOffsetOfReplacement) {
auto replacementRange = rangeAndOffsetOfReplacement->range;
if (replacementRange) {
bool restoreSelection = frameSelection.selection().isRange();
frame.editor().replaceRangeForSpellChecking(*replacementRange, [[annotatedString.string string] substringWithRange:NSMakeRange(relativeReplacementLocation, relativeReplacementLength + [annotatedString.string length] - length)]);
if (restoreSelection) {
uint64_t selectionLocationToRestore = locationInRoot - selectionOffset;
if (selectionLocationToRestore > locationInRoot + relativeReplacementLocation + relativeReplacementLength) {
auto selectionToRestore = createLiveRange(resolveCharacterRange(makeRangeSelectingNodeContents(*root), { selectionLocationToRestore, 0 }));
frameSelection.moveTo(selectionToRestore.ptr());
}
}
}
}
}
[annotatedString.string enumerateAttributesInRange:NSMakeRange(0, [annotatedString.string length]) options:0 usingBlock:^(NSDictionary<NSAttributedStringKey, id> *attrs, NSRange attributeRange, BOOL *stop) {
auto attributeCoreRange = resolveCharacterRange(makeRangeSelectingNodeContents(*root), { locationInRoot + attributeRange.location, attributeRange.length });
[attrs enumerateKeysAndObjectsUsingBlock:^(NSAttributedStringKey key, id value, BOOL *stop) {
if (![value isKindOfClass:[NSString class]])
return;
markers.addPlatformTextCheckingMarker(attributeCoreRange, key, (NSString *)value);
// FIXME: Switch to constants after rdar://problem/48914153 is resolved.
if ([key isEqualToString:@"NSSpellingState"]) {
NSSpellingState spellingState = (NSSpellingState)[value integerValue];
if (spellingState & NSSpellingStateSpellingFlag)
markers.addMarker(attributeCoreRange, DocumentMarker::Spelling);
if (spellingState & NSSpellingStateGrammarFlag) {
NSString *userDescription = [attrs objectForKey:@"NSGrammarUserDescription"];
markers.addMarker(attributeCoreRange, DocumentMarker::Grammar, userDescription);
}
}
}];
}];
}
void TextCheckingControllerProxy::removeAnnotationRelativeToSelection(const String& annotation, int64_t selectionOffset, uint64_t length)
{
Frame& frame = m_page.corePage()->focusController().focusedOrMainFrame();
auto rangeAndOffset = rangeAndOffsetRelativeToSelection(selectionOffset, length);
if (!rangeAndOffset)
return;
auto range = rangeAndOffset->range;
if (!range)
return;
bool removeCoreSpellingMarkers = annotation == "NSSpellingState";
frame.document()->markers().filterMarkers(*range, [&] (DocumentMarker* marker) {
if (!WTF::holds_alternative<DocumentMarker::PlatformTextCheckingData>(marker->data()))
return false;
auto& textCheckingData = WTF::get<DocumentMarker::PlatformTextCheckingData>(marker->data());
return textCheckingData.key != annotation;
}, removeCoreSpellingMarkers ? relevantMarkerTypes() : DocumentMarker::PlatformTextChecking);
}
AttributedString TextCheckingControllerProxy::annotatedSubstringBetweenPositions(const WebCore::VisiblePosition& start, const WebCore::VisiblePosition& end)
{
auto startBoundary = makeBoundaryPoint(start);
auto endBoundary = makeBoundaryPoint(end);
if (!startBoundary || !endBoundary)
return { };
auto entireRange = SimpleRange { *startBoundary, *endBoundary };
auto string = adoptNS([[NSMutableAttributedString alloc] init]);
for (TextIterator it(entireRange); !it.atEnd(); it.advance()) {
if (!it.text().length())
continue;
[string appendAttributedString:adoptNS([[NSAttributedString alloc] initWithString:it.text().createNSStringWithoutCopying().get()]).get()];
auto range = it.range();
auto markers = range.start.document().markers().markersInRange(createLiveRange(range), DocumentMarker::PlatformTextChecking);
for (const auto* marker : markers) {
if (!WTF::holds_alternative<DocumentMarker::PlatformTextCheckingData>(marker->data()))
continue;
auto& data = WTF::get<DocumentMarker::PlatformTextCheckingData>(marker->data());
auto subrange = resolveCharacterRange(range, { marker->startOffset(), marker->endOffset() - marker->startOffset() });
[string addAttribute:data.key value:data.value range:characterRange(entireRange, subrange)];
}
}
return { { WTFMove(string) } };
}
} // namespace WebKit
#endif // ENABLE(PLATFORM_DRIVEN_TEXT_CHECKING)