Context menu doesn't account for selection semantics
[WebKit-https.git] / Source / WebCore / editing / mac / DictionaryLookup.mm
1 /*
2  * Copyright (C) 2014 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 #import "config.h"
27 #import "DictionaryLookup.h"
28
29 #if PLATFORM(MAC)
30
31 #import "Document.h"
32 #import "FocusController.h"
33 #import "Frame.h"
34 #import "FrameSelection.h"
35 #import "HTMLConverter.h"
36 #import "HitTestResult.h"
37 #import "LookupSPI.h"
38 #import "Page.h"
39 #import "Range.h"
40 #import "RenderObject.h"
41 #import "TextIterator.h"
42 #import "VisiblePosition.h"
43 #import "VisibleSelection.h"
44 #import "VisibleUnits.h"
45 #import "WebCoreSystemInterface.h"
46 #import "htmlediting.h"
47 #import <PDFKit/PDFKit.h>
48 #import <wtf/RefPtr.h>
49
50 namespace WebCore {
51
52 bool isPositionInRange(const VisiblePosition& position, Range* range)
53 {
54     RefPtr<Range> positionRange = makeRange(position, position);
55
56     ExceptionCode ec = 0;
57     range->compareBoundaryPoints(Range::START_TO_START, positionRange.get(), ec);
58     if (ec)
59         return false;
60
61     if (!range->isPointInRange(positionRange->startContainer(), positionRange->startOffset(), ec))
62         return false;
63     if (ec)
64         return false;
65
66     return true;
67 }
68
69 bool shouldUseSelection(const VisiblePosition& position, const VisibleSelection& selection)
70 {
71     if (!selection.isRange())
72         return false;
73
74     RefPtr<Range> selectedRange = selection.toNormalizedRange();
75     if (!selectedRange)
76         return false;
77
78     return isPositionInRange(position, selectedRange.get());
79 }
80
81 PassRefPtr<Range> rangeExpandedAroundPositionByCharacters(const VisiblePosition& position, int numberOfCharactersToExpand)
82 {
83     Position start = position.deepEquivalent();
84     Position end = position.deepEquivalent();
85     for (int i = 0; i < numberOfCharactersToExpand; ++i) {
86         start = start.previous(Character);
87         end = end.next(Character);
88     }
89
90     return makeRange(start, end);
91 }
92
93 PassRefPtr<Range> rangeForDictionaryLookupForSelection(const VisibleSelection& selection, NSDictionary **options)
94 {
95     RefPtr<Range> selectedRange = selection.toNormalizedRange();
96     if (!selectedRange)
97         return nullptr;
98
99     VisiblePosition selectionStart = selection.visibleStart();
100     VisiblePosition selectionEnd = selection.visibleEnd();
101
102     // As context, we are going to use the surrounding paragraphs of text.
103     VisiblePosition paragraphStart = startOfParagraph(selectionStart);
104     VisiblePosition paragraphEnd = endOfParagraph(selectionEnd);
105
106     int lengthToSelectionStart = TextIterator::rangeLength(makeRange(paragraphStart, selectionStart).get());
107     int lengthToSelectionEnd = TextIterator::rangeLength(makeRange(paragraphStart, selectionEnd).get());
108     NSRange rangeToPass = NSMakeRange(lengthToSelectionStart, lengthToSelectionEnd - lengthToSelectionStart);
109
110     String fullPlainTextString = plainText(makeRange(paragraphStart, paragraphEnd).get());
111
112     // Since we already have the range we want, we just need to grab the returned options.
113     if (Class luLookupDefinitionModule = getLULookupDefinitionModuleClass())
114         [luLookupDefinitionModule tokenRangeForString:fullPlainTextString range:rangeToPass options:options];
115
116     return selectedRange.release();
117 }
118
119 PassRefPtr<Range> rangeForDictionaryLookupAtHitTestResult(const HitTestResult& hitTestResult, NSDictionary **options)
120 {
121     Node* node = hitTestResult.innerNonSharedNode();
122     if (!node)
123         return nullptr;
124
125     auto renderer = node->renderer();
126     if (!renderer)
127         return nullptr;
128
129     Frame* frame = node->document().frame();
130     if (!frame)
131         return nullptr;
132
133     // Don't do anything if there is no character at the point.
134     IntPoint framePoint = hitTestResult.roundedPointInInnerNodeFrame();
135     if (!frame->rangeForPoint(framePoint))
136         return nullptr;
137
138     VisiblePosition position = frame->visiblePositionForPoint(framePoint);
139     if (position.isNull())
140         position = firstPositionInOrBeforeNode(node);
141
142     VisibleSelection selection = frame->page()->focusController().focusedOrMainFrame().selection().selection();
143     if (shouldUseSelection(position, selection))
144         return rangeForDictionaryLookupForSelection(selection, options);
145
146     VisibleSelection selectionAccountingForLineRules = VisibleSelection(position);
147     selectionAccountingForLineRules.expandUsingGranularity(WordGranularity);
148     position = selectionAccountingForLineRules.start();
149
150     // As context, we are going to use 250 characters of text before and after the point.
151     RefPtr<Range> fullCharacterRange = rangeExpandedAroundPositionByCharacters(position, 250);
152     if (!fullCharacterRange)
153         return nullptr;
154
155     NSRange rangeToPass = NSMakeRange(TextIterator::rangeLength(makeRange(fullCharacterRange->startPosition(), position).get()), 0);
156
157     String fullPlainTextString = plainText(fullCharacterRange.get());
158
159     NSRange extractedRange = NSMakeRange(rangeToPass.location, 0);
160     if (Class luLookupDefinitionModule = getLULookupDefinitionModuleClass())
161         extractedRange = [luLookupDefinitionModule tokenRangeForString:fullPlainTextString range:rangeToPass options:options];
162
163     // This function sometimes returns {NSNotFound, 0} if it was unable to determine a good string.
164     if (extractedRange.location == NSNotFound)
165         return nullptr;
166
167     return TextIterator::subrange(fullCharacterRange.get(), extractedRange.location, extractedRange.length);
168 }
169
170 static void expandSelectionByCharacters(PDFSelection *selection, NSInteger numberOfCharactersToExpand, NSInteger& charactersAddedBeforeStart, NSInteger& charactersAddedAfterEnd)
171 {
172     size_t originalLength = selection.string.length;
173     [selection extendSelectionAtStart:numberOfCharactersToExpand];
174     
175     charactersAddedBeforeStart = selection.string.length - originalLength;
176     
177     [selection extendSelectionAtEnd:numberOfCharactersToExpand];
178     charactersAddedAfterEnd = selection.string.length - originalLength - charactersAddedBeforeStart;
179 }
180
181 NSString *dictionaryLookupForPDFSelection(PDFSelection *selection, NSDictionary **options)
182 {
183     // Don't do anything if there is no character at the point.
184     if (!selection || !selection.string.length)
185         return @"";
186     
187     RetainPtr<PDFSelection> selectionForLookup = adoptNS([selection copy]);
188     
189     // As context, we are going to use 250 characters of text before and after the point.
190     NSInteger originalLength = [selectionForLookup string].length;
191     NSInteger charactersAddedBeforeStart = 0;
192     NSInteger charactersAddedAfterEnd = 0;
193     expandSelectionByCharacters(selectionForLookup.get(), 250, charactersAddedBeforeStart, charactersAddedAfterEnd);
194     
195     NSString *fullPlainTextString = [selectionForLookup string];
196     NSRange rangeToPass = NSMakeRange(charactersAddedBeforeStart, 0);
197     
198     NSRange extractedRange = NSMakeRange(rangeToPass.location, 0);
199     if (Class luLookupDefinitionModule = getLULookupDefinitionModuleClass())
200         extractedRange = [luLookupDefinitionModule tokenRangeForString:fullPlainTextString range:rangeToPass options:options];
201     
202     // This function sometimes returns {NSNotFound, 0} if it was unable to determine a good string.
203     if (extractedRange.location == NSNotFound)
204         return selection.string;
205     
206     NSInteger lookupAddedBefore = (extractedRange.location < rangeToPass.location) ? rangeToPass.location - extractedRange.location : 0;
207     NSInteger lookupAddedAfter = 0;
208     if ((extractedRange.location + extractedRange.length) > (rangeToPass.location + originalLength))
209         lookupAddedAfter = (extractedRange.location + extractedRange.length) - (rangeToPass.location + originalLength);
210     
211     [selection extendSelectionAtStart:lookupAddedBefore];
212     [selection extendSelectionAtEnd:lookupAddedAfter];
213     
214     ASSERT([selection.string isEqualToString:[fullPlainTextString substringWithRange:extractedRange]]);
215     return selection.string;
216 }
217
218 } // namespace WebCore
219
220 #endif // PLATFORM(MAC)
221