Title: [233412] trunk
Revision
233412
Author
[email protected]
Date
2018-07-01 17:47:47 -0700 (Sun, 01 Jul 2018)

Log Message

[macOS] Text replacements that end with symbols are expanded immediately
https://bugs.webkit.org/show_bug.cgi?id=187225
<rdar://problem/41112433>

Reviewed by Darin Adler.

Source/WebCore:

In shipping Safari, enabling grammar correction causes text shortcuts that end with symbols or punctuation marks
to immediately trigger when typing; normally, when grammar correction is off, this is only triggered after the
user has additionally inserted a punctuation mark or whitespace character after the replaced text.

This bug happens because enabling grammar checking causes the spell checking range to expand to the range of the
full sentence, so any text checking results that replace an existing range are triggered as long as they end
anywhere in the sentence. In contrast, when grammar checking is disabled, the spell checking range is limited to
the nearest adjacent word, which prevents text replacement from occurring elsewhere in the sentence.

However, after r232530, we now always expand the spell checking range to the extent of the sentence when a word
is typed regardless of whether grammar checking is enabled, which means that the issue described above now
happens everywhere. To fix this recent regression and the existing bug, we:

-   Augment our spellchecking codepaths to include a new automatic text replacement range, alongside
    spellchecking and paragraph ranges.
-   Let this automatic text replacement range be the range of the adjacent word in the case where the user has
    finished typing a word.
-   When marking and replacing text checking results, consult this new automatic text replacement instead of the
    spellchecking range.

This keeps the behavior grammar and sentence retro correction results intact, while limiting the scope in which
text replacement results are applied.

Test: editing/spelling/text-replacement-after-typing-to-word.html

* editing/AlternativeTextController.cpp:
(WebCore::AlternativeTextController::timerFired):
* editing/Editor.cpp:
(WebCore::Editor::replaceSelectionWithFragment):
(WebCore::Editor::markMisspellingsAfterTypingToWord):

Pass in the adjacent word range for the `automaticReplacementRange`, instead of the spell checking range (which
may be extended to the full range of the sentence).

(WebCore::Editor::markAllMisspellingsAndBadGrammarInRanges):

Add an `automaticReplacementRange` argument to markAllMisspellingsAndBadGrammarInRanges, and adjust call sites
to pass in a range (generally the same as the spell checking range, but in the case where a word has been typed,
this is a narrower range).

(WebCore::correctSpellcheckingPreservingTextCheckingParagraph):
(WebCore::Editor::markAndReplaceFor):

When replacing text, only allow text replacement in the automatic replacement range rather than the spell
checking range.

(WebCore::Editor::markMisspellingsAndBadGrammar):
* editing/Editor.h:
* editing/SpellChecker.cpp:
(WebCore::SpellCheckRequest::SpellCheckRequest):

Add a new version of this constructor that takes a single Range representing both the spellchecking range and
the automatic text replacement range, for convenience.

(WebCore::SpellCheckRequest::create):
* editing/SpellChecker.h:

Add plumbing for the automatic replacement range.

(WebCore::SpellCheckRequest::automaticReplacementRange const):
* editing/TextCheckingHelper.cpp:

Add plumbing for the automatic replacement range, and new helpers to locate the range as offsets within the
text checking paragraph range.

(WebCore::TextCheckingParagraph::TextCheckingParagraph):
(WebCore::TextCheckingParagraph::invalidateParagraphRangeValues):
(WebCore::TextCheckingParagraph::automaticReplacementStart const):
(WebCore::TextCheckingParagraph::automaticReplacementLength const):
* editing/TextCheckingHelper.h:

Tools:

Adds testing support for mocking NSSpellChecker's text checking results. See below for more details, and the new
layout test for an example of its usage.

* DumpRenderTree/DumpRenderTree.xcodeproj/project.pbxproj:
* DumpRenderTree/TestRunner.cpp:
(setSpellCheckerTextReplacementsCallback):

Add bindings support in TestRunner to specify a set of text replacement mappings. Each entry in the dictionary
maps a string representing an input to the NSSpellChecker to some information describing the spell checking
result that LayoutTestSpellChecker will return.

(TestRunner::staticFunctions):
* DumpRenderTree/TestRunner.h:
* DumpRenderTree/mac/DumpRenderTree.mm:
(resetWebViewToConsistentStateBeforeTesting):

Restore the original shared NSSpellchecker before transitioning to the next layout test, if needed.

* DumpRenderTree/mac/DumpRenderTreeSpellChecker.mm: Removed.
* DumpRenderTree/mac/TestRunnerMac.mm:
(TestRunner::setSpellCheckerLoggingEnabled):
(TestRunner::setSpellCheckerTextReplacements):

These testRunner methods ensure that `-[NSSpellChecker sharedSpellChecker]` is swizzled to return our mock
LayoutTestSpellChecker instance before calling into it.

* DumpRenderTree/win/TestRunnerWin.cpp:
(TestRunner::setSpellCheckerTextReplacements):
* TestRunnerShared/cocoa/LayoutTestSpellChecker.h: Renamed from Tools/DumpRenderTree/mac/DumpRenderTreeSpellChecker.h.
* TestRunnerShared/cocoa/LayoutTestSpellChecker.mm: Added.

Moved DumpRenderTreeSpellChecker to LayoutTestSpellChecker, and made it compile for both WebKitTestRunner
(modern WebKit) and DumpRenderTree (legacy WebKit) by putting it in TestRunnerShared.

(existingGlobalLayoutTestSpellChecker):
(ensureGlobalLayoutTestSpellChecker):
(stringForCorrectionResponse):
(nsTextCheckingType):
(-[LayoutTestTextCheckingResult initWithType:range:replacement:]):
(-[LayoutTestTextCheckingResult range]):
(-[LayoutTestTextCheckingResult resultType]):
(-[LayoutTestTextCheckingResult replacementString]):
(-[LayoutTestTextCheckingResult description]):

LayoutTestTextCheckingResult represents a fake NSTextCheckingResult containing spell checking results supplied
by the layout test.

(+[LayoutTestSpellChecker installIfNecessary]):
(+[LayoutTestSpellChecker uninstallAndReset]):

Helper methods to begin and end swizzling the shared NSSpellChecker.

(-[LayoutTestSpellChecker reset]):

Resets the state of the LayoutTestSpellChecker (this entails clearing the fake replacements dictionary and
turning off logging for recorded spellchecking responses).

(-[LayoutTestSpellChecker replacements]):
(-[LayoutTestSpellChecker setReplacements:]):
(-[LayoutTestSpellChecker setReplacementsFromJSObject:inContext:]):

Helper method to take a `JSObjectRef` supplied by the test runner and transform it into a map of spell checking
string inputs to LayoutTestTextCheckingResults.

(-[LayoutTestSpellChecker checkString:range:types:options:inSpellDocumentWithTag:orthography:wordCount:]):

Consult the text replacement map and bail early if a match is found, before calling into real NSSpellChecker
logic to perform spellchecking.

(-[LayoutTestSpellChecker recordResponse:toCorrection:forWord:language:inSpellDocumentWithTag:]):

Reimplement the original functionality in DumpRenderTreeSpellChecker to make `-recordResponse:…` print to stdout
by overriding the method and printing if the `-spellCheckerLoggingEnabled` property has been set to YES.

* WebKitTestRunner/WebKitTestRunner.xcodeproj/project.pbxproj:

LayoutTests:

Adds a layout test to check that if a user has configured a text replacement that ends with punctuation, then:
1. Typing that text replacement won't immediately trigger replacement.
2. Text replacement is triggered after pressing enter.

* editing/spelling/text-replacement-after-typing-to-word-expected.txt: Added.
* editing/spelling/text-replacement-after-typing-to-word.html: Added.
* platform/ios/TestExpectations:
* platform/mac-wk2/TestExpectations:

Modified Paths

Added Paths

Removed Paths

Diff

Modified: trunk/LayoutTests/ChangeLog (233411 => 233412)


--- trunk/LayoutTests/ChangeLog	2018-07-01 23:51:40 UTC (rev 233411)
+++ trunk/LayoutTests/ChangeLog	2018-07-02 00:47:47 UTC (rev 233412)
@@ -1,3 +1,20 @@
+2018-07-01  Wenson Hsieh  <[email protected]>
+
+        [macOS] Text replacements that end with symbols are expanded immediately
+        https://bugs.webkit.org/show_bug.cgi?id=187225
+        <rdar://problem/41112433>
+
+        Reviewed by Darin Adler.
+
+        Adds a layout test to check that if a user has configured a text replacement that ends with punctuation, then:
+        1. Typing that text replacement won't immediately trigger replacement.
+        2. Text replacement is triggered after pressing enter.
+
+        * editing/spelling/text-replacement-after-typing-to-word-expected.txt: Added.
+        * editing/spelling/text-replacement-after-typing-to-word.html: Added.
+        * platform/ios/TestExpectations:
+        * platform/mac-wk2/TestExpectations:
+
 2018-06-29  Antoine Quint  <[email protected]>
 
         [Web Animations] Make WPT test at timing-model/timelines/document-timelines.html pass reliably

Added: trunk/LayoutTests/editing/spelling/text-replacement-after-typing-to-word-expected.txt (0 => 233412)


--- trunk/LayoutTests/editing/spelling/text-replacement-after-typing-to-word-expected.txt	                        (rev 0)
+++ trunk/LayoutTests/editing/spelling/text-replacement-after-typing-to-word-expected.txt	2018-07-02 00:47:47 UTC (rev 233412)
@@ -0,0 +1,8 @@
+Before pressing enter:
+PASS editor.textContent is 'YT?'
+PASS successfullyParsed is true
+
+TEST COMPLETE
+You there?
+
+

Added: trunk/LayoutTests/editing/spelling/text-replacement-after-typing-to-word.html (0 => 233412)


--- trunk/LayoutTests/editing/spelling/text-replacement-after-typing-to-word.html	                        (rev 0)
+++ trunk/LayoutTests/editing/spelling/text-replacement-after-typing-to-word.html	2018-07-02 00:47:47 UTC (rev 233412)
@@ -0,0 +1,63 @@
+<html>
+<head>
+<script src=""
+<script src=""
+<script>
+jsTestIsAsync = true;
+
+function zeroDelayTimer()
+{
+    return new Promise(resolve => setTimeout(resolve, 0));
+}
+
+async function runTest()
+{
+    if (window.internals) {
+        internals.settings.setUnifiedTextCheckerEnabled(true);
+        internals.settings.setAsynchronousSpellCheckingEnabled(false);
+        internals.setAutomaticTextReplacementEnabled(true);
+        internals.setAutomaticSpellingCorrectionEnabled(true);
+        testRunner.setSpellCheckerTextReplacements({
+            "YT?": {
+                "replacement": "You there?",
+                "type": "replacement",
+                "from": 0,
+                "to": 3
+            },
+            "YT?\n": {
+                "replacement": "You there?",
+                "type": "replacement",
+                "from": 0,
+                "to": 3
+            }
+        });
+    }
+
+    const editor = document.getElementById("editor");
+    editor.focus();
+    for (const character of "YT?")
+        typeCharacterCommand(character);
+    await zeroDelayTimer();
+
+    if (!window.testRunner) {
+        description.innerHTML = `To manually test, add an automatic text replacement mapping from the string "YT?" to
+        "You there?", and then type the string "YT?". It should not be immediately corrected to "You there?".
+        However, entering a newline should subsequently trigger autocorrection.`;
+        return;
+    }
+
+    debug("Before pressing enter:");
+    shouldBe("editor.textContent", "'YT?'");
+    insertParagraphCommand();
+    await zeroDelayTimer();
+    finishJSTest();
+}
+</script>
+</head>
+
+<body _onload_="runTest()">
+    <div id="description"></div>
+    <div contenteditable style="margin-bottom: 1em; border: 1px orange dashed;" id="editor"></div>
+    <script src=""
+</body>
+</html>

Modified: trunk/LayoutTests/platform/ios/TestExpectations (233411 => 233412)


--- trunk/LayoutTests/platform/ios/TestExpectations	2018-07-01 23:51:40 UTC (rev 233411)
+++ trunk/LayoutTests/platform/ios/TestExpectations	2018-07-02 00:47:47 UTC (rev 233412)
@@ -110,6 +110,9 @@
 fast/writing-mode/english-bt-text-with-spelling-marker.html [ WontFix ]
 fast/writing-mode/english-rl-text-with-spelling-marker.html [ WontFix ]
 
+# Requires support for testing text replacement
+editing/spelling/text-replacement-after-typing-to-word.html [ Skip ]
+
 # UIKit draws selection on iOS
 fast/selectors/input-with-selection-pseudo-element.html [ WontFix ]
 fast/selectors/selection-window-inactive.html [ WontFix ]

Modified: trunk/LayoutTests/platform/mac-wk2/TestExpectations (233411 => 233412)


--- trunk/LayoutTests/platform/mac-wk2/TestExpectations	2018-07-01 23:51:40 UTC (rev 233411)
+++ trunk/LayoutTests/platform/mac-wk2/TestExpectations	2018-07-02 00:47:47 UTC (rev 233412)
@@ -180,6 +180,7 @@
 webkit.org/b/105616 editing/spelling/grammar.html
 webkit.org/b/105616 editing/spelling/markers.html
 webkit.org/b/105616 editing/spelling/retro-correction-spelling-markers.html
+webkit.org/b/105616 editing/spelling/text-replacement-after-typing-to-word.html
 webkit.org/b/105616 editing/spelling/spelling-markers-after-pasting-sentence.html
 webkit.org/b/105616 editing/mac/spelling/autocorrection-delete.html [ Failure ]
 webkit.org/b/105616 editing/mac/spelling/autocorrection-removing-underline-after-paste.html [ Failure ]

Modified: trunk/Source/WebCore/ChangeLog (233411 => 233412)


--- trunk/Source/WebCore/ChangeLog	2018-07-01 23:51:40 UTC (rev 233411)
+++ trunk/Source/WebCore/ChangeLog	2018-07-02 00:47:47 UTC (rev 233412)
@@ -1,3 +1,82 @@
+2018-07-01  Wenson Hsieh  <[email protected]>
+
+        [macOS] Text replacements that end with symbols are expanded immediately
+        https://bugs.webkit.org/show_bug.cgi?id=187225
+        <rdar://problem/41112433>
+
+        Reviewed by Darin Adler.
+
+        In shipping Safari, enabling grammar correction causes text shortcuts that end with symbols or punctuation marks
+        to immediately trigger when typing; normally, when grammar correction is off, this is only triggered after the
+        user has additionally inserted a punctuation mark or whitespace character after the replaced text.
+
+        This bug happens because enabling grammar checking causes the spell checking range to expand to the range of the
+        full sentence, so any text checking results that replace an existing range are triggered as long as they end
+        anywhere in the sentence. In contrast, when grammar checking is disabled, the spell checking range is limited to
+        the nearest adjacent word, which prevents text replacement from occurring elsewhere in the sentence.
+
+        However, after r232530, we now always expand the spell checking range to the extent of the sentence when a word
+        is typed regardless of whether grammar checking is enabled, which means that the issue described above now
+        happens everywhere. To fix this recent regression and the existing bug, we:
+
+        -   Augment our spellchecking codepaths to include a new automatic text replacement range, alongside
+            spellchecking and paragraph ranges.
+        -   Let this automatic text replacement range be the range of the adjacent word in the case where the user has
+            finished typing a word.
+        -   When marking and replacing text checking results, consult this new automatic text replacement instead of the
+            spellchecking range.
+
+        This keeps the behavior grammar and sentence retro correction results intact, while limiting the scope in which
+        text replacement results are applied.
+
+        Test: editing/spelling/text-replacement-after-typing-to-word.html
+
+        * editing/AlternativeTextController.cpp:
+        (WebCore::AlternativeTextController::timerFired):
+        * editing/Editor.cpp:
+        (WebCore::Editor::replaceSelectionWithFragment):
+        (WebCore::Editor::markMisspellingsAfterTypingToWord):
+
+        Pass in the adjacent word range for the `automaticReplacementRange`, instead of the spell checking range (which
+        may be extended to the full range of the sentence).
+
+        (WebCore::Editor::markAllMisspellingsAndBadGrammarInRanges):
+
+        Add an `automaticReplacementRange` argument to markAllMisspellingsAndBadGrammarInRanges, and adjust call sites
+        to pass in a range (generally the same as the spell checking range, but in the case where a word has been typed,
+        this is a narrower range).
+
+        (WebCore::correctSpellcheckingPreservingTextCheckingParagraph):
+        (WebCore::Editor::markAndReplaceFor):
+
+        When replacing text, only allow text replacement in the automatic replacement range rather than the spell
+        checking range.
+
+        (WebCore::Editor::markMisspellingsAndBadGrammar):
+        * editing/Editor.h:
+        * editing/SpellChecker.cpp:
+        (WebCore::SpellCheckRequest::SpellCheckRequest):
+
+        Add a new version of this constructor that takes a single Range representing both the spellchecking range and
+        the automatic text replacement range, for convenience.
+
+        (WebCore::SpellCheckRequest::create):
+        * editing/SpellChecker.h:
+
+        Add plumbing for the automatic replacement range.
+
+        (WebCore::SpellCheckRequest::automaticReplacementRange const):
+        * editing/TextCheckingHelper.cpp:
+
+        Add plumbing for the automatic replacement range, and new helpers to locate the range as offsets within the
+        text checking paragraph range.
+
+        (WebCore::TextCheckingParagraph::TextCheckingParagraph):
+        (WebCore::TextCheckingParagraph::invalidateParagraphRangeValues):
+        (WebCore::TextCheckingParagraph::automaticReplacementStart const):
+        (WebCore::TextCheckingParagraph::automaticReplacementLength const):
+        * editing/TextCheckingHelper.h:
+
 2018-06-30  David Kilzer  <[email protected]>
 
         Follow-up: Fix clang static analyzer warnings: Garbage return value

Modified: trunk/Source/WebCore/editing/AlternativeTextController.cpp (233411 => 233412)


--- trunk/Source/WebCore/editing/AlternativeTextController.cpp	2018-07-01 23:51:40 UTC (rev 233411)
+++ trunk/Source/WebCore/editing/AlternativeTextController.cpp	2018-07-02 00:47:47 UTC (rev 233412)
@@ -283,7 +283,8 @@
         VisiblePosition start(selection.start(), selection.affinity());
         VisiblePosition p = startOfWord(start, LeftWordIfOnBoundary);
         VisibleSelection adjacentWords = VisibleSelection(p, start);
-        m_frame.editor().markAllMisspellingsAndBadGrammarInRanges(TextCheckingTypeSpelling | TextCheckingTypeReplacement | TextCheckingTypeShowCorrectionPanel, adjacentWords.toNormalizedRange().get(), 0);
+        auto adjacentWordRange = adjacentWords.toNormalizedRange();
+        m_frame.editor().markAllMisspellingsAndBadGrammarInRanges(TextCheckingTypeSpelling | TextCheckingTypeReplacement | TextCheckingTypeShowCorrectionPanel, adjacentWordRange.get(), adjacentWordRange.get(), nullptr);
     }
         break;
     case AlternativeTextTypeReversion: {

Modified: trunk/Source/WebCore/editing/Editor.cpp (233411 => 233412)


--- trunk/Source/WebCore/editing/Editor.cpp	2018-07-01 23:51:40 UTC (rev 233411)
+++ trunk/Source/WebCore/editing/Editor.cpp	2018-07-02 00:47:47 UTC (rev 233412)
@@ -667,7 +667,7 @@
         return;
 
     auto rangeToCheck = Range::create(document(), firstPositionInNode(nodeToCheck), lastPositionInNode(nodeToCheck));
-    if (auto request = SpellCheckRequest::create(resolveTextCheckingTypeMask(*nodeToCheck, TextCheckingTypeSpelling | TextCheckingTypeGrammar), TextCheckingProcessBatch, rangeToCheck.copyRef(), rangeToCheck.copyRef()))
+    if (auto request = SpellCheckRequest::create(resolveTextCheckingTypeMask(*nodeToCheck, TextCheckingTypeSpelling | TextCheckingTypeGrammar), TextCheckingProcessBatch, rangeToCheck.copyRef(), rangeToCheck.copyRef(), rangeToCheck.copyRef()))
         m_spellChecker->requestCheckingFor(request.releaseNonNull());
 }
 
@@ -2342,7 +2342,8 @@
         return;
 
     VisibleSelection adjacentWords = VisibleSelection(startOfWord(wordStart, LeftWordIfOnBoundary), endOfWord(wordStart, RightWordIfOnBoundary));
-    markAllMisspellingsAndBadGrammarInRanges(textCheckingOptions, adjacentWords.toNormalizedRange().get(), adjacentWords.toNormalizedRange().get());
+    auto adjacentWordRange = adjacentWords.toNormalizedRange();
+    markAllMisspellingsAndBadGrammarInRanges(textCheckingOptions, adjacentWordRange.get(), adjacentWordRange.get(), adjacentWordRange.get());
 #else
 #if !USE(AUTOMATIC_TEXT_REPLACEMENT)
     UNUSED_PARAM(doReplacement);
@@ -2417,6 +2418,10 @@
         if (!spellCheckingRange)
             return;
 
+        auto adjacentWordRange = VisibleSelection(startOfWord(wordStart, LeftWordIfOnBoundary), endOfWord(wordStart, RightWordIfOnBoundary)).toNormalizedRange();
+        if (!adjacentWordRange)
+            return;
+
         // The spelling and grammar markers in these ranges are recomputed. This is because typing a word may
         // cause any other part of the current sentence to lose or gain spelling correction markers, due to
         // sentence retro correction. As such, we expand the spell checking range to encompass as much of the
@@ -2423,7 +2428,7 @@
         // full sentence as we can, respecting boundaries where spellchecking is disabled.
         fullSentenceRange->ownerDocument().markers().removeMarkers(fullSentenceRange.get(), DocumentMarker::Grammar);
         spellCheckingRange->ownerDocument().markers().removeMarkers(spellCheckingRange.get(), DocumentMarker::Spelling);
-        markAllMisspellingsAndBadGrammarInRanges(textCheckingOptions, spellCheckingRange.get(), fullSentenceRange.get());
+        markAllMisspellingsAndBadGrammarInRanges(textCheckingOptions, spellCheckingRange.get(), adjacentWordRange.get(), fullSentenceRange.get());
         return;
     }
 
@@ -2547,7 +2552,7 @@
 #endif
 }
 
-void Editor::markAllMisspellingsAndBadGrammarInRanges(TextCheckingTypeMask textCheckingOptions, Range* spellingRange, Range* grammarRange)
+void Editor::markAllMisspellingsAndBadGrammarInRanges(TextCheckingTypeMask textCheckingOptions, Range* spellingRange, Range* automaticReplacementRange, Range* grammarRange)
 {
     ASSERT(unifiedTextCheckerEnabled());
 
@@ -2579,7 +2584,8 @@
 
     // In asynchronous mode, we intentionally check paragraph-wide sentence.
     const auto resolvedOptions = resolveTextCheckingTypeMask(editableNode, textCheckingOptions);
-    auto request = SpellCheckRequest::create(resolvedOptions, TextCheckingProcessIncremental, asynchronous ? paragraphRange.get() : rangeToCheck, paragraphRange.copyRef());
+    auto& textReplacementRange = automaticReplacementRange ? *automaticReplacementRange : rangeToCheck;
+    auto request = SpellCheckRequest::create(resolvedOptions, TextCheckingProcessIncremental, asynchronous ? paragraphRange.get() : rangeToCheck, textReplacementRange, paragraphRange.copyRef());
     if (!request)
         return;
 
@@ -2627,7 +2633,8 @@
 
     RefPtr<Range> newParagraphRange = TextIterator::rangeFromLocationAndLength(&scope, paragraphLocation, paragraphLength + replacement.length() - resultLength);
 
-    paragraph = TextCheckingParagraph(TextIterator::subrange(*newParagraphRange, resultLocation, replacement.length()), newParagraphRange.get());
+    auto spellCheckingRange = TextIterator::subrange(*newParagraphRange, resultLocation, replacement.length());
+    paragraph = TextCheckingParagraph(spellCheckingRange.copyRef(), spellCheckingRange.copyRef(), newParagraphRange.get());
 }
 
 void Editor::markAndReplaceFor(const SpellCheckRequest& request, const Vector<TextCheckingResult>& results)
@@ -2635,7 +2642,7 @@
     Ref<Frame> protection(m_frame);
 
     TextCheckingTypeMask textCheckingOptions = request.data().mask();
-    TextCheckingParagraph paragraph(request.checkingRange(), &request.paragraphRange());
+    TextCheckingParagraph paragraph(request.checkingRange(), request.automaticReplacementRange(), &request.paragraphRange());
 
     const bool shouldMarkSpelling = textCheckingOptions & TextCheckingTypeSpelling;
     const bool shouldMarkGrammar = textCheckingOptions & TextCheckingTypeGrammar;
@@ -2675,6 +2682,7 @@
         const int resultLocation = results[i].location + offsetDueToReplacement;
         const int resultLength = results[i].length;
         const int resultEndLocation = resultLocation + resultLength;
+        const int automaticReplacementEndLocation = paragraph.automaticReplacementStart() + paragraph.automaticReplacementLength() + offsetDueToReplacement;
         const String& replacement = results[i].replacement;
         const bool resultEndsAtAmbiguousBoundary = useAmbiguousBoundaryOffset && selectionOffset - 1 <= resultEndLocation;
 
@@ -2699,12 +2707,12 @@
                     badGrammarRange->startContainer().document().markers().addMarker(badGrammarRange.ptr(), DocumentMarker::Grammar, detail.userDescription);
                 }
             }
-        } else if (resultEndLocation <= spellingRangeEndOffset && resultEndLocation >= paragraph.checkingStart()
+        } else if (resultEndLocation <= automaticReplacementEndLocation && resultEndLocation >= paragraph.automaticReplacementStart()
             && isAutomaticTextReplacementType(resultType)) {
-            // In this case the result range just has to touch the spelling range, so we can handle replacing non-word text such as punctuation.
+            // 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.
             ASSERT(resultLength > 0 && resultLocation >= 0);
 
-            if (shouldShowCorrectionPanel && (resultEndLocation < spellingRangeEndOffset
+            if (shouldShowCorrectionPanel && (resultEndLocation < automaticReplacementEndLocation
                 || !(resultType & (TextCheckingTypeReplacement | TextCheckingTypeCorrection))))
                 continue;
 
@@ -2728,7 +2736,7 @@
                 continue;
 
             if (shouldShowCorrectionPanel) {
-                if (resultEndLocation == spellingRangeEndOffset) {
+                if (resultEndLocation == automaticReplacementEndLocation) {
                     // We only show the correction panel on the last word.
                     m_alternativeTextController->show(rangeToReplace, replacement);
                     break;
@@ -2829,7 +2837,8 @@
         TextCheckingTypeMask textCheckingOptions = TextCheckingTypeSpelling;
         if (markGrammar && isGrammarCheckingEnabled())
             textCheckingOptions |= TextCheckingTypeGrammar;
-        markAllMisspellingsAndBadGrammarInRanges(textCheckingOptions, spellingSelection.toNormalizedRange().get(), grammarSelection.toNormalizedRange().get());
+        auto spellCheckingRange = spellingSelection.toNormalizedRange();
+        markAllMisspellingsAndBadGrammarInRanges(textCheckingOptions, spellCheckingRange.get(), spellCheckingRange.get(), grammarSelection.toNormalizedRange().get());
         return;
     }
 

Modified: trunk/Source/WebCore/editing/Editor.h (233411 => 233412)


--- trunk/Source/WebCore/editing/Editor.h	2018-07-01 23:51:40 UTC (rev 233411)
+++ trunk/Source/WebCore/editing/Editor.h	2018-07-02 00:47:47 UTC (rev 233412)
@@ -293,7 +293,7 @@
     bool isOverwriteModeEnabled() const { return m_overwriteModeEnabled; }
     WEBCORE_EXPORT void toggleOverwriteModeEnabled();
 
-    void markAllMisspellingsAndBadGrammarInRanges(TextCheckingTypeMask, Range* spellingRange, Range* grammarRange);
+    void markAllMisspellingsAndBadGrammarInRanges(TextCheckingTypeMask, Range* spellingRange, Range* automaticReplacementRange, Range* grammarRange);
 #if PLATFORM(IOS)
     NO_RETURN_DUE_TO_ASSERT
 #endif

Modified: trunk/Source/WebCore/editing/SpellChecker.cpp (233411 => 233412)


--- trunk/Source/WebCore/editing/SpellChecker.cpp	2018-07-01 23:51:40 UTC (rev 233411)
+++ trunk/Source/WebCore/editing/SpellChecker.cpp	2018-07-02 00:47:47 UTC (rev 233412)
@@ -40,8 +40,9 @@
 
 namespace WebCore {
 
-SpellCheckRequest::SpellCheckRequest(Ref<Range>&& checkingRange, Ref<Range>&& paragraphRange, const String& text, TextCheckingTypeMask mask, TextCheckingProcessType processType)
+SpellCheckRequest::SpellCheckRequest(Ref<Range>&& checkingRange, Ref<Range>&& automaticReplacementRange, Ref<Range>&& paragraphRange, const String& text, TextCheckingTypeMask mask, TextCheckingProcessType processType)
     : m_checkingRange(WTFMove(checkingRange))
+    , m_automaticReplacementRange(WTFMove(automaticReplacementRange))
     , m_paragraphRange(WTFMove(paragraphRange))
     , m_rootEditableElement(m_checkingRange->startContainer().rootEditableElement())
     , m_requestData(unrequestedTextCheckingSequence, text, mask, processType)
@@ -50,13 +51,13 @@
 
 SpellCheckRequest::~SpellCheckRequest() = default;
 
-RefPtr<SpellCheckRequest> SpellCheckRequest::create(TextCheckingTypeMask textCheckingOptions, TextCheckingProcessType processType, Ref<Range>&& checkingRange, Ref<Range>&& paragraphRange)
+RefPtr<SpellCheckRequest> SpellCheckRequest::create(TextCheckingTypeMask textCheckingOptions, TextCheckingProcessType processType, Ref<Range>&& checkingRange, Ref<Range>&& automaticReplacementRange, Ref<Range>&& paragraphRange)
 {
     String text = checkingRange->text();
     if (!text.length())
         return nullptr;
 
-    return adoptRef(*new SpellCheckRequest(WTFMove(checkingRange), WTFMove(paragraphRange), text, textCheckingOptions, processType));
+    return adoptRef(*new SpellCheckRequest(WTFMove(checkingRange), WTFMove(automaticReplacementRange), WTFMove(paragraphRange), text, textCheckingOptions, processType));
 }
 
 const TextCheckingRequestData& SpellCheckRequest::data() const

Modified: trunk/Source/WebCore/editing/SpellChecker.h (233411 => 233412)


--- trunk/Source/WebCore/editing/SpellChecker.h	2018-07-01 23:51:40 UTC (rev 233411)
+++ trunk/Source/WebCore/editing/SpellChecker.h	2018-07-02 00:47:47 UTC (rev 233412)
@@ -44,11 +44,12 @@
 
 class SpellCheckRequest : public TextCheckingRequest {
 public:
-    static RefPtr<SpellCheckRequest> create(TextCheckingTypeMask, TextCheckingProcessType, Ref<Range>&& checkingRange, Ref<Range>&& paragraphRange);
+    static RefPtr<SpellCheckRequest> create(TextCheckingTypeMask, TextCheckingProcessType, Ref<Range>&& checkingRange, Ref<Range>&& automaticReplacementRange, Ref<Range>&& paragraphRange);
     virtual ~SpellCheckRequest();
 
     Range& checkingRange() const { return m_checkingRange.get(); }
     Range& paragraphRange() const { return m_paragraphRange.get(); }
+    Range& automaticReplacementRange() const { return m_automaticReplacementRange.get(); }
     Element* rootEditableElement() const { return m_rootEditableElement.get(); }
 
     void setCheckerAndSequence(SpellChecker*, int sequence);
@@ -60,10 +61,11 @@
     void didCancel() override;
 
 private:
-    SpellCheckRequest(Ref<Range>&& checkingRange, Ref<Range>&& paragraphRange, const String&, TextCheckingTypeMask, TextCheckingProcessType);
+    SpellCheckRequest(Ref<Range>&& checkingRange, Ref<Range>&& automaticReplacementRange, Ref<Range>&& paragraphRange, const String&, TextCheckingTypeMask, TextCheckingProcessType);
 
     SpellChecker* m_checker { nullptr };
     Ref<Range> m_checkingRange;
+    Ref<Range> m_automaticReplacementRange;
     Ref<Range> m_paragraphRange;
     RefPtr<Element> m_rootEditableElement;
     TextCheckingRequestData m_requestData;

Modified: trunk/Source/WebCore/editing/TextCheckingHelper.cpp (233411 => 233412)


--- trunk/Source/WebCore/editing/TextCheckingHelper.cpp	2018-07-01 23:51:40 UTC (rev 233411)
+++ trunk/Source/WebCore/editing/TextCheckingHelper.cpp	2018-07-02 00:47:47 UTC (rev 233412)
@@ -117,12 +117,16 @@
     return paragraphRange;
 }
 
-TextCheckingParagraph::TextCheckingParagraph(Ref<Range>&& checkingRange, Range* paragraphRange)
+TextCheckingParagraph::TextCheckingParagraph(Ref<Range>&& checkingAndAutomaticReplacementRange)
+    : m_checkingRange(checkingAndAutomaticReplacementRange.copyRef())
+    , m_automaticReplacementRange(checkingAndAutomaticReplacementRange.copyRef())
+{
+}
+
+TextCheckingParagraph::TextCheckingParagraph(Ref<Range>&& checkingRange, Ref<Range>&& automaticReplacementRange, RefPtr<Range>&& paragraphRange)
     : m_checkingRange(WTFMove(checkingRange))
-    , m_paragraphRange(paragraphRange)
-    , m_checkingStart(-1)
-    , m_checkingEnd(-1)
-    , m_checkingLength(-1)
+    , m_automaticReplacementRange(WTFMove(automaticReplacementRange))
+    , m_paragraphRange(WTFMove(paragraphRange))
 {
 }
 
@@ -135,6 +139,8 @@
 void TextCheckingParagraph::invalidateParagraphRangeValues()
 {
     m_checkingStart = m_checkingEnd = -1;
+    m_automaticReplacementStart = -1;
+    m_automaticReplacementLength = -1;
     m_offsetAsRange = nullptr;
     m_text = String();
 }
@@ -211,6 +217,26 @@
     return m_checkingLength;
 }
 
+int TextCheckingParagraph::automaticReplacementStart() const
+{
+    if (m_automaticReplacementStart != -1)
+        return m_automaticReplacementStart;
+
+    auto startOffsetRange = Range::create(paragraphRange().startContainer().document(), paragraphRange().startPosition(), m_automaticReplacementRange->startPosition());
+    m_automaticReplacementStart = TextIterator::rangeLength(startOffsetRange.ptr());
+    return m_automaticReplacementStart;
+}
+
+int TextCheckingParagraph::automaticReplacementLength() const
+{
+    if (m_automaticReplacementLength != -1)
+        return m_automaticReplacementLength;
+
+    auto endOffsetRange = Range::create(paragraphRange().startContainer().document(), paragraphRange().startPosition(), m_automaticReplacementRange->endPosition());
+    m_automaticReplacementLength = TextIterator::rangeLength(endOffsetRange.ptr()) - automaticReplacementStart();
+    return m_automaticReplacementLength;
+}
+
 TextCheckingHelper::TextCheckingHelper(EditorClient& client, Range& range)
     : m_client(client)
     , m_range(range)

Modified: trunk/Source/WebCore/editing/TextCheckingHelper.h (233411 => 233412)


--- trunk/Source/WebCore/editing/TextCheckingHelper.h	2018-07-01 23:51:40 UTC (rev 233411)
+++ trunk/Source/WebCore/editing/TextCheckingHelper.h	2018-07-02 00:47:47 UTC (rev 233412)
@@ -33,7 +33,8 @@
 
 class TextCheckingParagraph {
 public:
-    explicit TextCheckingParagraph(Ref<Range>&& checkingRange, Range* paragraphRange = nullptr);
+    explicit TextCheckingParagraph(Ref<Range>&& checkingAndAutomaticReplacementRange);
+    explicit TextCheckingParagraph(Ref<Range>&& checkingRange, Ref<Range>&& automaticReplacementRange, RefPtr<Range>&& paragraphRange);
 
     int rangeLength() const;
     Ref<Range> subrange(int characterOffset, int characterCount) const;
@@ -55,6 +56,11 @@
     int checkingLength() const;
     String checkingSubstring() const { return textSubstring(checkingStart(), checkingLength()); }
 
+    // Determines the range in which we allow automatic text replacement. If an automatic replacement range is not passed to the
+    // text checking paragraph, this defaults to the spell checking range.
+    int automaticReplacementStart() const;
+    int automaticReplacementLength() const;
+
     bool checkingRangeMatches(int location, int length) const { return location == checkingStart() && length == checkingLength(); }
     bool isCheckingRangeCoveredBy(int location, int length) const { return location <= checkingStart() && location + length >= checkingStart() + checkingLength(); }
     bool checkingRangeCovers(int location, int length) const { return location < checkingEnd() && location + length > checkingStart(); }
@@ -65,12 +71,15 @@
     Range& offsetAsRange() const;
 
     Ref<Range> m_checkingRange;
+    Ref<Range> m_automaticReplacementRange;
     mutable RefPtr<Range> m_paragraphRange;
     mutable RefPtr<Range> m_offsetAsRange;
     mutable String m_text;
-    mutable int m_checkingStart;
-    mutable int m_checkingEnd;
-    mutable int m_checkingLength;
+    mutable int m_checkingStart { -1 };
+    mutable int m_checkingEnd { -1 };
+    mutable int m_checkingLength { -1 };
+    mutable int m_automaticReplacementStart { -1 };
+    mutable int m_automaticReplacementLength { -1 };
 };
 
 class TextCheckingHelper {

Modified: trunk/Tools/ChangeLog (233411 => 233412)


--- trunk/Tools/ChangeLog	2018-07-01 23:51:40 UTC (rev 233411)
+++ trunk/Tools/ChangeLog	2018-07-02 00:47:47 UTC (rev 233412)
@@ -1,3 +1,87 @@
+2018-07-01  Wenson Hsieh  <[email protected]>
+
+        [macOS] Text replacements that end with symbols are expanded immediately
+        https://bugs.webkit.org/show_bug.cgi?id=187225
+        <rdar://problem/41112433>
+
+        Reviewed by Darin Adler.
+
+        Adds testing support for mocking NSSpellChecker's text checking results. See below for more details, and the new
+        layout test for an example of its usage.
+
+        * DumpRenderTree/DumpRenderTree.xcodeproj/project.pbxproj:
+        * DumpRenderTree/TestRunner.cpp:
+        (setSpellCheckerTextReplacementsCallback):
+
+        Add bindings support in TestRunner to specify a set of text replacement mappings. Each entry in the dictionary
+        maps a string representing an input to the NSSpellChecker to some information describing the spell checking
+        result that LayoutTestSpellChecker will return.
+
+        (TestRunner::staticFunctions):
+        * DumpRenderTree/TestRunner.h:
+        * DumpRenderTree/mac/DumpRenderTree.mm:
+        (resetWebViewToConsistentStateBeforeTesting):
+
+        Restore the original shared NSSpellchecker before transitioning to the next layout test, if needed.
+
+        * DumpRenderTree/mac/DumpRenderTreeSpellChecker.mm: Removed.
+        * DumpRenderTree/mac/TestRunnerMac.mm:
+        (TestRunner::setSpellCheckerLoggingEnabled):
+        (TestRunner::setSpellCheckerTextReplacements):
+
+        These testRunner methods ensure that `-[NSSpellChecker sharedSpellChecker]` is swizzled to return our mock
+        LayoutTestSpellChecker instance before calling into it.
+
+        * DumpRenderTree/win/TestRunnerWin.cpp:
+        (TestRunner::setSpellCheckerTextReplacements):
+        * TestRunnerShared/cocoa/LayoutTestSpellChecker.h: Renamed from Tools/DumpRenderTree/mac/DumpRenderTreeSpellChecker.h.
+        * TestRunnerShared/cocoa/LayoutTestSpellChecker.mm: Added.
+
+        Moved DumpRenderTreeSpellChecker to LayoutTestSpellChecker, and made it compile for both WebKitTestRunner
+        (modern WebKit) and DumpRenderTree (legacy WebKit) by putting it in TestRunnerShared.
+
+        (existingGlobalLayoutTestSpellChecker):
+        (ensureGlobalLayoutTestSpellChecker):
+        (stringForCorrectionResponse):
+        (nsTextCheckingType):
+        (-[LayoutTestTextCheckingResult initWithType:range:replacement:]):
+        (-[LayoutTestTextCheckingResult range]):
+        (-[LayoutTestTextCheckingResult resultType]):
+        (-[LayoutTestTextCheckingResult replacementString]):
+        (-[LayoutTestTextCheckingResult description]):
+
+        LayoutTestTextCheckingResult represents a fake NSTextCheckingResult containing spell checking results supplied
+        by the layout test.
+
+        (+[LayoutTestSpellChecker installIfNecessary]):
+        (+[LayoutTestSpellChecker uninstallAndReset]):
+
+        Helper methods to begin and end swizzling the shared NSSpellChecker.
+
+        (-[LayoutTestSpellChecker reset]):
+
+        Resets the state of the LayoutTestSpellChecker (this entails clearing the fake replacements dictionary and
+        turning off logging for recorded spellchecking responses).
+
+        (-[LayoutTestSpellChecker replacements]):
+        (-[LayoutTestSpellChecker setReplacements:]):
+        (-[LayoutTestSpellChecker setReplacementsFromJSObject:inContext:]):
+
+        Helper method to take a `JSObjectRef` supplied by the test runner and transform it into a map of spell checking
+        string inputs to LayoutTestTextCheckingResults.
+
+        (-[LayoutTestSpellChecker checkString:range:types:options:inSpellDocumentWithTag:orthography:wordCount:]):
+
+        Consult the text replacement map and bail early if a match is found, before calling into real NSSpellChecker
+        logic to perform spellchecking.
+
+        (-[LayoutTestSpellChecker recordResponse:toCorrection:forWord:language:inSpellDocumentWithTag:]):
+
+        Reimplement the original functionality in DumpRenderTreeSpellChecker to make `-recordResponse:…` print to stdout
+        by overriding the method and printing if the `-spellCheckerLoggingEnabled` property has been set to YES.
+
+        * WebKitTestRunner/WebKitTestRunner.xcodeproj/project.pbxproj:
+
 2018-07-01  Thibault Saunier  <[email protected]>
 
         [WPE][GTK] Fix retrieving backtrace from within flatpak sandbox in test runner

Modified: trunk/Tools/DumpRenderTree/DumpRenderTree.xcodeproj/project.pbxproj (233411 => 233412)


--- trunk/Tools/DumpRenderTree/DumpRenderTree.xcodeproj/project.pbxproj	2018-07-01 23:51:40 UTC (rev 233411)
+++ trunk/Tools/DumpRenderTree/DumpRenderTree.xcodeproj/project.pbxproj	2018-07-02 00:47:47 UTC (rev 233412)
@@ -77,7 +77,6 @@
 		29CFBA2E12273A1000BC30C0 /* AccessibilityTextMarkerMac.mm in Sources */ = {isa = PBXBuildFile; fileRef = 29CFBA2D12273A1000BC30C0 /* AccessibilityTextMarkerMac.mm */; };
 		2CE88FA217124D8C00734FC0 /* _javascript_Threading.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 2CE88FA117124CEE00734FC0 /* _javascript_Threading.cpp */; };
 		2D403F1B15087209005358D2 /* LayoutTestHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 2D403EA215087142005358D2 /* LayoutTestHelper.m */; };
-		2DA2E3A51E1BA54100A3BBD0 /* DumpRenderTreeSpellChecker.mm in Sources */ = {isa = PBXBuildFile; fileRef = 2DA2E3A41E1BA54100A3BBD0 /* DumpRenderTreeSpellChecker.mm */; };
 		31117B3D15D9A56A00163BC8 /* MockWebNotificationProvider.mm in Sources */ = {isa = PBXBuildFile; fileRef = 31117B3B15D9A56A00163BC8 /* MockWebNotificationProvider.mm */; };
 		312943F91E71F2B4001EE2CC /* IOSLayoutTestCommunication.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3148A0551E6F90F400D3B316 /* IOSLayoutTestCommunication.cpp */; };
 		4464CABE1C20A08B00E5BB55 /* DumpRenderTreeAppMain.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4464CABD1C20A07000E5BB55 /* DumpRenderTreeAppMain.mm */; };
@@ -148,6 +147,7 @@
 		C23EA2081BC9F05100C980B7 /* FontWithFeatures.otf in Copy Font Files */ = {isa = PBXBuildFile; fileRef = C23EA2061BC9EABA00C980B7 /* FontWithFeatures.otf */; };
 		C23EA2091BC9F05100C980B7 /* FontWithFeatures.ttf in Copy Font Files */ = {isa = PBXBuildFile; fileRef = C23EA2071BC9EABA00C980B7 /* FontWithFeatures.ttf */; };
 		E1B7816511AF31B7007E1BC2 /* MockGeolocationProvider.mm in Sources */ = {isa = PBXBuildFile; fileRef = E1B7808711AF1669007E1BC2 /* MockGeolocationProvider.mm */; };
+		F4C3578D20E8444E00FA0748 /* LayoutTestSpellChecker.mm in Sources */ = {isa = PBXBuildFile; fileRef = F4C3578820E8442700FA0748 /* LayoutTestSpellChecker.mm */; };
 		F4D423611DD5048200678290 /* TextInputControllerIOS.m in Sources */ = {isa = PBXBuildFile; fileRef = F4D4235F1DD5045300678290 /* TextInputControllerIOS.m */; };
 /* End PBXBuildFile section */
 
@@ -282,8 +282,6 @@
 		2CE88FA117124CEE00734FC0 /* _javascript_Threading.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = _javascript_Threading.cpp; sourceTree = "<group>"; };
 		2D403EA215087142005358D2 /* LayoutTestHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = LayoutTestHelper.m; path = mac/LayoutTestHelper.m; sourceTree = "<group>"; };
 		2D403F19150871F9005358D2 /* LayoutTestHelper */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = LayoutTestHelper; sourceTree = BUILT_PRODUCTS_DIR; };
-		2DA2E3A31E1BA54100A3BBD0 /* DumpRenderTreeSpellChecker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DumpRenderTreeSpellChecker.h; path = mac/DumpRenderTreeSpellChecker.h; sourceTree = "<group>"; };
-		2DA2E3A41E1BA54100A3BBD0 /* DumpRenderTreeSpellChecker.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = DumpRenderTreeSpellChecker.mm; path = mac/DumpRenderTreeSpellChecker.mm; sourceTree = "<group>"; };
 		2EDE0DAA1F5131DE00D5F8DF /* AppKitTestSPI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = AppKitTestSPI.h; path = mac/AppKitTestSPI.h; sourceTree = "<group>"; };
 		31117B3A15D9A56A00163BC8 /* MockWebNotificationProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MockWebNotificationProvider.h; path = mac/MockWebNotificationProvider.h; sourceTree = "<group>"; };
 		31117B3B15D9A56A00163BC8 /* MockWebNotificationProvider.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = MockWebNotificationProvider.mm; path = mac/MockWebNotificationProvider.mm; sourceTree = "<group>"; };
@@ -412,6 +410,8 @@
 		C23EA2071BC9EABA00C980B7 /* FontWithFeatures.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = FontWithFeatures.ttf; path = fonts/FontWithFeatures.ttf; sourceTree = "<group>"; };
 		E1B7808511AF1643007E1BC2 /* MockGeolocationProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MockGeolocationProvider.h; path = mac/MockGeolocationProvider.h; sourceTree = "<group>"; };
 		E1B7808711AF1669007E1BC2 /* MockGeolocationProvider.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = MockGeolocationProvider.mm; path = mac/MockGeolocationProvider.mm; sourceTree = "<group>"; };
+		F4C3578820E8442700FA0748 /* LayoutTestSpellChecker.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = LayoutTestSpellChecker.mm; path = ../TestRunnerShared/cocoa/LayoutTestSpellChecker.mm; sourceTree = "<group>"; };
+		F4C3578920E8442700FA0748 /* LayoutTestSpellChecker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = LayoutTestSpellChecker.h; path = ../TestRunnerShared/cocoa/LayoutTestSpellChecker.h; sourceTree = "<group>"; };
 		F4D4235F1DD5045300678290 /* TextInputControllerIOS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TextInputControllerIOS.m; path = ios/TextInputControllerIOS.m; sourceTree = "<group>"; };
 		F4D423601DD5046900678290 /* TextInputController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TextInputController.h; sourceTree = "<group>"; };
 /* End PBXFileReference section */
@@ -687,6 +687,7 @@
 		3148A0651E6F90F400D3B316 /* TestRunnerShared */ = {
 			isa = PBXGroup;
 			children = (
+				F4B6C31820E84382008AC225 /* cocoa */,
 				3148A0551E6F90F400D3B316 /* IOSLayoutTestCommunication.cpp */,
 				3148A0561E6F90F400D3B316 /* IOSLayoutTestCommunication.h */,
 			);
@@ -779,8 +780,6 @@
 			children = (
 				A8B91ADF0CF3B372008F91FF /* DumpRenderTreePasteboard.h */,
 				A8B91AD70CF3B32F008F91FF /* DumpRenderTreePasteboard.mm */,
-				2DA2E3A31E1BA54100A3BBD0 /* DumpRenderTreeSpellChecker.h */,
-				2DA2E3A41E1BA54100A3BBD0 /* DumpRenderTreeSpellChecker.mm */,
 				A8B91ADD0CF3B372008F91FF /* DumpRenderTreeWindow.h */,
 				A8B91AD90CF3B32F008F91FF /* DumpRenderTreeWindow.mm */,
 			);
@@ -817,6 +816,15 @@
 			name = PixelDump;
 			sourceTree = "<group>";
 		};
+		F4B6C31820E84382008AC225 /* cocoa */ = {
+			isa = PBXGroup;
+			children = (
+				F4C3578920E8442700FA0748 /* LayoutTestSpellChecker.h */,
+				F4C3578820E8442700FA0748 /* LayoutTestSpellChecker.mm */,
+			);
+			name = cocoa;
+			sourceTree = "<group>";
+		};
 		F4D4235D1DD4F99900678290 /* mac */ = {
 			isa = PBXGroup;
 			children = (
@@ -1081,7 +1089,6 @@
 				BCA18B7B0C9B08F100114369 /* DumpRenderTreeDraggingInfo.mm in Sources */,
 				A8D79CEB0FC28B2C004AC8FE /* DumpRenderTreeFileDraggingSource.m in Sources */,
 				A8B91ADA0CF3B32F008F91FF /* DumpRenderTreePasteboard.mm in Sources */,
-				2DA2E3A51E1BA54100A3BBD0 /* DumpRenderTreeSpellChecker.mm in Sources */,
 				A8B91ADC0CF3B32F008F91FF /* DumpRenderTreeWindow.mm in Sources */,
 				BCA18B620C9B08C200114369 /* EditingDelegate.mm in Sources */,
 				BCA18B700C9B08DB00114369 /* EventSendingController.mm in Sources */,
@@ -1093,6 +1100,7 @@
 				2CE88FA217124D8C00734FC0 /* _javascript_Threading.cpp in Sources */,
 				0F18E7061D6BA0230027E547 /* JSUIScriptController.cpp in Sources */,
 				0F18E7131D6BC43A0027E547 /* JSWrapper.cpp in Sources */,
+				F4C3578D20E8444E00FA0748 /* LayoutTestSpellChecker.mm in Sources */,
 				E1B7816511AF31B7007E1BC2 /* MockGeolocationProvider.mm in Sources */,
 				31117B3D15D9A56A00163BC8 /* MockWebNotificationProvider.mm in Sources */,
 				BCA18B720C9B08DB00114369 /* NavigationController.m in Sources */,

Modified: trunk/Tools/DumpRenderTree/TestRunner.cpp (233411 => 233412)


--- trunk/Tools/DumpRenderTree/TestRunner.cpp	2018-07-01 23:51:40 UTC (rev 233411)
+++ trunk/Tools/DumpRenderTree/TestRunner.cpp	2018-07-02 00:47:47 UTC (rev 233412)
@@ -1788,6 +1788,16 @@
     return JSValueMakeUndefined(context);
 }
 
+static JSValueRef setSpellCheckerTextReplacementsCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
+{
+    if (argumentCount < 1)
+        return JSValueMakeUndefined(context);
+
+    auto* runner = static_cast<TestRunner*>(JSObjectGetPrivate(thisObject));
+    runner->setSpellCheckerTextReplacements(context, JSValueToObject(context, arguments[0], nullptr));
+    return JSValueMakeUndefined(context);
+}
+
 static JSValueRef setOpenPanelFilesCallback(JSContextRef context, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
 {
     if (argumentCount == 1)
@@ -2255,6 +2265,7 @@
         { "runUIScript", runUIScriptCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
         { "imageCountInGeneralPasteboard", imageCountInGeneralPasteboardCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
         { "setSpellCheckerLoggingEnabled", setSpellCheckerLoggingEnabledCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
+        { "setSpellCheckerTextReplacements", setSpellCheckerTextReplacementsCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
         { "setOpenPanelFiles", setOpenPanelFilesCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
         { "forceImmediateCompletion", forceImmediateCompletionCallback, kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontDelete },
         { 0, 0, 0 }

Modified: trunk/Tools/DumpRenderTree/TestRunner.h (233411 => 233412)


--- trunk/Tools/DumpRenderTree/TestRunner.h	2018-07-01 23:51:40 UTC (rev 233411)
+++ trunk/Tools/DumpRenderTree/TestRunner.h	2018-07-02 00:47:47 UTC (rev 233412)
@@ -376,6 +376,7 @@
     bool dumpJSConsoleLogInStdErr() const { return m_dumpJSConsoleLogInStdErr; }
 
     void setSpellCheckerLoggingEnabled(bool);
+    void setSpellCheckerTextReplacements(JSContextRef, JSObjectRef replacements);
 
     const std::vector<std::string>& openPanelFiles() const { return m_openPanelFiles; }
     void setOpenPanelFiles(JSContextRef, JSValueRef);

Modified: trunk/Tools/DumpRenderTree/mac/DumpRenderTree.mm (233411 => 233412)


--- trunk/Tools/DumpRenderTree/mac/DumpRenderTree.mm	2018-07-01 23:51:40 UTC (rev 233411)
+++ trunk/Tools/DumpRenderTree/mac/DumpRenderTree.mm	2018-07-02 00:47:47 UTC (rev 233412)
@@ -34,7 +34,6 @@
 #import "DefaultPolicyDelegate.h"
 #import "DumpRenderTreeDraggingInfo.h"
 #import "DumpRenderTreePasteboard.h"
-#import "DumpRenderTreeSpellChecker.h"
 #import "DumpRenderTreeWindow.h"
 #import "EditingDelegate.h"
 #import "EventSendingController.h"
@@ -41,6 +40,7 @@
 #import "FrameLoadDelegate.h"
 #import "HistoryDelegate.h"
 #import "_javascript_Threading.h"
+#import "LayoutTestSpellChecker.h"
 #import "MockGeolocationProvider.h"
 #import "MockWebNotificationProvider.h"
 #import "NavigationController.h"
@@ -1868,7 +1868,9 @@
 
     [mainFrame _clearOpener];
 
-    setSpellCheckerLoggingEnabled(false);
+#if PLATFORM(MAC)
+    [LayoutTestSpellChecker uninstallAndReset];
+#endif
 
     resetAccumulatedLogs();
     WebCoreTestSupport::initializeLogChannelsIfNecessary();

Deleted: trunk/Tools/DumpRenderTree/mac/DumpRenderTreeSpellChecker.h (233411 => 233412)


--- trunk/Tools/DumpRenderTree/mac/DumpRenderTreeSpellChecker.h	2018-07-01 23:51:40 UTC (rev 233411)
+++ trunk/Tools/DumpRenderTree/mac/DumpRenderTreeSpellChecker.h	2018-07-02 00:47:47 UTC (rev 233412)
@@ -1,26 +0,0 @@
-/*
- * Copyright (C) 2017 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.
- */
-
-void setSpellCheckerLoggingEnabled(bool);

Deleted: trunk/Tools/DumpRenderTree/mac/DumpRenderTreeSpellChecker.mm (233411 => 233412)


--- trunk/Tools/DumpRenderTree/mac/DumpRenderTreeSpellChecker.mm	2018-07-01 23:51:40 UTC (rev 233411)
+++ trunk/Tools/DumpRenderTree/mac/DumpRenderTreeSpellChecker.mm	2018-07-02 00:47:47 UTC (rev 233412)
@@ -1,91 +0,0 @@
-/*
- * Copyright (C) 2017 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 "DumpRenderTreeSpellChecker.h"
-
-#if PLATFORM(MAC)
-
-#import <AppKit/NSSpellChecker.h>
-#import <wtf/Assertions.h>
-#import <wtf/ObjcRuntimeExtras.h>
-
-static bool spellCheckerLoggingEnabled;
-static IMP appKitRecordResponseIMP;
-
-static const char *stringForCorrectionResponse(NSCorrectionResponse correctionResponse)
-{
-    switch (correctionResponse) {
-    case NSCorrectionResponseNone:
-        return "none";
-    case NSCorrectionResponseAccepted:
-        return "accepted";
-    case NSCorrectionResponseRejected:
-        return "rejected";
-    case NSCorrectionResponseIgnored:
-        return "ignored";
-    case NSCorrectionResponseEdited:
-        return "edited";
-    case NSCorrectionResponseReverted:
-        return "reverted";
-    }
-
-    return "invalid";
-}
-
-static void drt_NSSpellChecker_recordResponseToCorrection(id self, SEL _cmd, NSCorrectionResponse response, NSString *correction, NSString *word, NSString *language, NSInteger spellDocumentTag)
-{
-    if (spellCheckerLoggingEnabled)
-        printf("NSSpellChecker recordResponseToCorrection: %s -> %s (response: %s)\n", [word UTF8String], [correction UTF8String], stringForCorrectionResponse(response));
-
-    wtfCallIMP<void, NSCorrectionResponse, NSString *, NSString *, NSString *, NSInteger>(appKitRecordResponseIMP, self, _cmd, response, correction, word, language, spellDocumentTag);
-}
-
-static void swizzleNSSpellCheckerMethodsIfNeeded()
-{
-    static bool hasSwizzled;
-    if (hasSwizzled)
-        return;
-    hasSwizzled = true;
-
-    Method recordResponseMethod = class_getInstanceMethod(objc_getClass("NSSpellChecker"), @selector(recordResponse:toCorrection:forWord:language:inSpellDocumentWithTag:));
-
-    appKitRecordResponseIMP = method_setImplementation(recordResponseMethod, (IMP)drt_NSSpellChecker_recordResponseToCorrection);
-}
-
-void setSpellCheckerLoggingEnabled(bool enabled)
-{
-    swizzleNSSpellCheckerMethodsIfNeeded();
-
-    spellCheckerLoggingEnabled = enabled;
-}
-
-#else // PLATFORM(MAC)
-
-void setSpellCheckerLoggingEnabled(bool)
-{
-}
-
-#endif // PLATFORM(MAC)

Modified: trunk/Tools/DumpRenderTree/mac/TestRunnerMac.mm (233411 => 233412)


--- trunk/Tools/DumpRenderTree/mac/TestRunnerMac.mm	2018-07-01 23:51:40 UTC (rev 233411)
+++ trunk/Tools/DumpRenderTree/mac/TestRunnerMac.mm	2018-07-02 00:47:47 UTC (rev 233412)
@@ -31,8 +31,8 @@
 #import "TestRunner.h"
 
 #import "DefaultPolicyDelegate.h"
-#import "DumpRenderTreeSpellChecker.h"
 #import "EditingDelegate.h"
+#import "LayoutTestSpellChecker.h"
 #import "MockGeolocationProvider.h"
 #import "MockWebNotificationProvider.h"
 #import "PolicyDelegate.h"
@@ -212,9 +212,23 @@
 
 void TestRunner::setSpellCheckerLoggingEnabled(bool enabled)
 {
-    ::setSpellCheckerLoggingEnabled(enabled);
+#if PLATFORM(MAC)
+    [LayoutTestSpellChecker checker].spellCheckerLoggingEnabled = enabled;
+#else
+    UNUSED_PARAM(enabled);
+#endif
 }
 
+void TestRunner::setSpellCheckerTextReplacements(JSContextRef context, JSObjectRef replacements)
+{
+#if PLATFORM(MAC)
+    [[LayoutTestSpellChecker checker] setReplacementsFromJSObject:replacements inContext:context];
+#else
+    UNUSED_PARAM(replacements);
+    UNUSED_PARAM(context);
+#endif
+}
+
 void TestRunner::closeIdleLocalStorageDatabases()
 {
     [WebStorageManager closeIdleLocalStorageDatabases];

Modified: trunk/Tools/DumpRenderTree/win/TestRunnerWin.cpp (233411 => 233412)


--- trunk/Tools/DumpRenderTree/win/TestRunnerWin.cpp	2018-07-01 23:51:40 UTC (rev 233411)
+++ trunk/Tools/DumpRenderTree/win/TestRunnerWin.cpp	2018-07-02 00:47:47 UTC (rev 233412)
@@ -1406,3 +1406,8 @@
 {
     fprintf(testResult, "ERROR: TestRunner::setSpellCheckerLoggingEnabled() not implemented\n");
 }
+
+void TestRunner::setSpellCheckerTextReplacements(JSContextRef, JSObjectRef)
+{
+    fprintf(testResult, "ERROR: TestRunner::setSpellCheckerTextReplacements() not implemented\n");
+}

Copied: trunk/Tools/TestRunnerShared/cocoa/LayoutTestSpellChecker.h (from rev 233411, trunk/Tools/DumpRenderTree/mac/DumpRenderTreeSpellChecker.h) (0 => 233412)


--- trunk/Tools/TestRunnerShared/cocoa/LayoutTestSpellChecker.h	                        (rev 0)
+++ trunk/Tools/TestRunnerShared/cocoa/LayoutTestSpellChecker.h	2018-07-02 00:47:47 UTC (rev 233412)
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2018 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.
+ */
+
+#pragma once
+
+#import <_javascript_Core/_javascript_Core.h>
+#import <wtf/RetainPtr.h>
+
+#if PLATFORM(MAC)
+
+#import <AppKit/NSSpellChecker.h>
+
+@class LayoutTestTextCheckingResult;
+
+@interface LayoutTestSpellChecker : NSSpellChecker {
+@private
+    RetainPtr<NSDictionary<NSString *, LayoutTestTextCheckingResult *>> _replacements;
+    BOOL _spellCheckerLoggingEnabled;
+}
+
++ (instancetype)checker;
++ (void)uninstallAndReset;
+
+- (void)setReplacementsFromJSObject:(JSObjectRef)replacements inContext:(JSContextRef)context;
+@property (nonatomic, copy) NSDictionary<NSString *, LayoutTestTextCheckingResult *> *replacements;
+@property (nonatomic) BOOL spellCheckerLoggingEnabled;
+@end
+
+#endif // PLATFORM(MAC)

Added: trunk/Tools/TestRunnerShared/cocoa/LayoutTestSpellChecker.mm (0 => 233412)


--- trunk/Tools/TestRunnerShared/cocoa/LayoutTestSpellChecker.mm	                        (rev 0)
+++ trunk/Tools/TestRunnerShared/cocoa/LayoutTestSpellChecker.mm	2018-07-02 00:47:47 UTC (rev 233412)
@@ -0,0 +1,249 @@
+/*
+ * Copyright (C) 2018 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 "LayoutTestSpellChecker.h"
+
+#import <_javascript_Core/JSRetainPtr.h>
+#import <objc/runtime.h>
+#import <wtf/Assertions.h>
+
+#if PLATFORM(MAC)
+
+static LayoutTestSpellChecker *globalSpellChecker = nil;
+static BOOL hasSwizzledLayoutTestSpellChecker = NO;
+static IMP globallySwizzledSharedSpellCheckerImplementation;
+static Method originalSharedSpellCheckerMethod;
+
+static LayoutTestSpellChecker *ensureGlobalLayoutTestSpellChecker()
+{
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        globalSpellChecker = [[LayoutTestSpellChecker alloc] init];
+    });
+    return globalSpellChecker;
+}
+
+static const char *stringForCorrectionResponse(NSCorrectionResponse correctionResponse)
+{
+    switch (correctionResponse) {
+    case NSCorrectionResponseNone:
+        return "none";
+    case NSCorrectionResponseAccepted:
+        return "accepted";
+    case NSCorrectionResponseRejected:
+        return "rejected";
+    case NSCorrectionResponseIgnored:
+        return "ignored";
+    case NSCorrectionResponseEdited:
+        return "edited";
+    case NSCorrectionResponseReverted:
+        return "reverted";
+    }
+    return "invalid";
+}
+
+static NSTextCheckingType nsTextCheckingType(JSRetainPtr<JSStringRef>&& jsType)
+{
+    auto cfType = adoptCF(JSStringCopyCFString(kCFAllocatorDefault, jsType.get()));
+    if (CFStringCompare(cfType.get(), CFSTR("orthography"), kCFCompareCaseInsensitive) == kCFCompareEqualTo)
+        return NSTextCheckingTypeOrthography;
+
+    if (CFStringCompare(cfType.get(), CFSTR("spelling"), kCFCompareCaseInsensitive) == kCFCompareEqualTo)
+        return NSTextCheckingTypeSpelling;
+
+    if (CFStringCompare(cfType.get(), CFSTR("grammar"), kCFCompareCaseInsensitive) == kCFCompareEqualTo)
+        return NSTextCheckingTypeGrammar;
+
+    if (CFStringCompare(cfType.get(), CFSTR("date"), kCFCompareCaseInsensitive) == kCFCompareEqualTo)
+        return NSTextCheckingTypeDate;
+
+    if (CFStringCompare(cfType.get(), CFSTR("address"), kCFCompareCaseInsensitive) == kCFCompareEqualTo)
+        return NSTextCheckingTypeAddress;
+
+    if (CFStringCompare(cfType.get(), CFSTR("link"), kCFCompareCaseInsensitive) == kCFCompareEqualTo)
+        return NSTextCheckingTypeLink;
+
+    if (CFStringCompare(cfType.get(), CFSTR("quote"), kCFCompareCaseInsensitive) == kCFCompareEqualTo)
+        return NSTextCheckingTypeQuote;
+
+    if (CFStringCompare(cfType.get(), CFSTR("dash"), kCFCompareCaseInsensitive) == kCFCompareEqualTo)
+        return NSTextCheckingTypeDash;
+
+    if (CFStringCompare(cfType.get(), CFSTR("replacement"), kCFCompareCaseInsensitive) == kCFCompareEqualTo)
+        return NSTextCheckingTypeReplacement;
+
+    if (CFStringCompare(cfType.get(), CFSTR("correction"), kCFCompareCaseInsensitive) == kCFCompareEqualTo)
+        return NSTextCheckingTypeCorrection;
+
+    if (CFStringCompare(cfType.get(), CFSTR("regular-_expression_"), kCFCompareCaseInsensitive) == kCFCompareEqualTo)
+        return NSTextCheckingTypeRegularExpression;
+
+    if (CFStringCompare(cfType.get(), CFSTR("phone-number"), kCFCompareCaseInsensitive) == kCFCompareEqualTo)
+        return NSTextCheckingTypePhoneNumber;
+
+    if (CFStringCompare(cfType.get(), CFSTR("transit-information"), kCFCompareCaseInsensitive) == kCFCompareEqualTo)
+        return NSTextCheckingTypeTransitInformation;
+
+    ASSERT_NOT_REACHED();
+    return NSTextCheckingTypeSpelling;
+}
+
+@interface LayoutTestTextCheckingResult : NSTextCheckingResult {
+@private
+    RetainPtr<NSString> _replacement;
+    NSTextCheckingType _type;
+    NSRange _range;
+}
+
+- (instancetype)initWithType:(NSTextCheckingType)type range:(NSRange)range replacement:(NSString *)replacement;
+@end
+
+@implementation LayoutTestTextCheckingResult
+
+- (instancetype)initWithType:(NSTextCheckingType)type range:(NSRange)range replacement:(NSString *)replacement
+{
+    if (!(self = [super init]))
+        return nil;
+
+    _type = type;
+    _range = range;
+    _replacement = replacement;
+
+    return self;
+}
+
+- (NSRange)range
+{
+    return _range;
+}
+
+- (NSTextCheckingType)resultType
+{
+    return _type;
+}
+
+- (NSString *)replacementString
+{
+    return _replacement.get();
+}
+
+- (NSString *)description
+{
+    return [NSString stringWithFormat:@"<%@ %p type=%tu range=[%tu, %tu] replacement='%@'>", self.class, self, _type, _range.location, _range.location + _range.length, _replacement.get()];
+}
+
+@end
+
+@implementation LayoutTestSpellChecker
+
+@synthesize spellCheckerLoggingEnabled=_spellCheckerLoggingEnabled;
+
++ (instancetype)checker
+{
+    auto *spellChecker = ensureGlobalLayoutTestSpellChecker();
+    if (hasSwizzledLayoutTestSpellChecker)
+        return spellChecker;
+
+    originalSharedSpellCheckerMethod = class_getClassMethod(objc_getMetaClass("NSSpellChecker"), @selector(sharedSpellChecker));
+    globallySwizzledSharedSpellCheckerImplementation = method_setImplementation(originalSharedSpellCheckerMethod, reinterpret_cast<IMP>(ensureGlobalLayoutTestSpellChecker));
+    hasSwizzledLayoutTestSpellChecker = YES;
+    return spellChecker;
+}
+
++ (void)uninstallAndReset
+{
+    [globalSpellChecker reset];
+    if (!hasSwizzledLayoutTestSpellChecker)
+        return;
+
+    method_setImplementation(originalSharedSpellCheckerMethod, globallySwizzledSharedSpellCheckerImplementation);
+    hasSwizzledLayoutTestSpellChecker = NO;
+}
+
+- (void)reset
+{
+    self.replacements = nil;
+    self.spellCheckerLoggingEnabled = NO;
+}
+
+- (NSDictionary<NSString *, LayoutTestTextCheckingResult *> *)replacements
+{
+    return _replacements.get();
+}
+
+- (void)setReplacements:(NSDictionary<NSString *, LayoutTestTextCheckingResult *> *)replacements
+{
+    _replacements = adoptNS(replacements.copy);
+}
+
+- (void)setReplacementsFromJSObject:(JSObjectRef)replacements inContext:(JSContextRef)context
+{
+    auto fromPropertyName = adopt(JSStringCreateWithUTF8CString("from"));
+    auto toPropertyName = adopt(JSStringCreateWithUTF8CString("to"));
+    auto typePropertyName = adopt(JSStringCreateWithUTF8CString("type"));
+    auto replacementPropertyName = adopt(JSStringCreateWithUTF8CString("replacement"));
+    auto properties = JSObjectCopyPropertyNames(context, replacements);
+    auto result = adoptNS([[NSMutableDictionary alloc] init]);
+    for (size_t index = 0; index < JSPropertyNameArrayGetCount(properties); ++index) {
+        JSStringRef wordToReplace = JSPropertyNameArrayGetNameAtIndex(properties, index);
+        JSObjectRef replacement = JSValueToObject(context, JSObjectGetProperty(context, replacements, wordToReplace, nullptr), nullptr);
+        long fromValue = lroundl(JSValueToNumber(context, JSObjectGetProperty(context, replacement, fromPropertyName.get(), nullptr), nullptr));
+        long toValue = lroundl(JSValueToNumber(context, JSObjectGetProperty(context, replacement, toPropertyName.get(), nullptr), nullptr));
+        auto typeValue = adopt(JSValueToStringCopy(context, JSObjectGetProperty(context, replacement, typePropertyName.get(), nullptr), nullptr));
+        auto replacementValue = JSObjectGetProperty(context, replacement, replacementPropertyName.get(), nullptr);
+        RetainPtr<CFStringRef> replacementText;
+        if (!JSValueIsUndefined(context, replacementValue)) {
+            auto replacementJSString = adopt(JSValueToStringCopy(context, replacementValue, nullptr));
+            replacementText = adoptCF(JSStringCopyCFString(kCFAllocatorDefault, replacementJSString.get()));
+        }
+        auto spellCheckResult = adoptNS([[LayoutTestTextCheckingResult alloc] initWithType:nsTextCheckingType(WTFMove(typeValue)) range:NSMakeRange(fromValue, toValue - fromValue) replacement:(NSString *)replacementText.get()]);
+        auto cfWordToReplace = adoptCF(JSStringCopyCFString(kCFAllocatorDefault, wordToReplace));
+        [result setObject:spellCheckResult.get() forKey:(NSString *)cfWordToReplace.get()];
+    }
+    JSPropertyNameArrayRelease(properties);
+
+    _replacements = WTFMove(result);
+}
+
+- (NSArray<NSTextCheckingResult *> *)checkString:(NSString *)stringToCheck range:(NSRange)range types:(NSTextCheckingTypes)checkingTypes options:(NSDictionary<NSString *, id> *)options inSpellDocumentWithTag:(NSInteger)tag orthography:(NSOrthography **)orthography wordCount:(NSInteger *)wordCount
+{
+    if (auto *result = [_replacements objectForKey:stringToCheck])
+        return @[ result ];
+
+    return [super checkString:stringToCheck range:range types:checkingTypes options:options inSpellDocumentWithTag:tag orthography:orthography wordCount:wordCount];
+}
+
+- (void)recordResponse:(NSCorrectionResponse)response toCorrection:(NSString *)correction forWord:(NSString *)word language:(NSString *)language inSpellDocumentWithTag:(NSInteger)tag
+{
+    if (_spellCheckerLoggingEnabled)
+        printf("NSSpellChecker recordResponseToCorrection: %s -> %s (response: %s)\n", [word UTF8String], [correction UTF8String], stringForCorrectionResponse(response));
+
+    [super recordResponse:response toCorrection:correction forWord:word language:language inSpellDocumentWithTag:tag];
+}
+
+@end
+
+#endif // PLATFORM(MAC)

Modified: trunk/Tools/WebKitTestRunner/WebKitTestRunner.xcodeproj/project.pbxproj (233411 => 233412)


--- trunk/Tools/WebKitTestRunner/WebKitTestRunner.xcodeproj/project.pbxproj	2018-07-01 23:51:40 UTC (rev 233411)
+++ trunk/Tools/WebKitTestRunner/WebKitTestRunner.xcodeproj/project.pbxproj	2018-07-02 00:47:47 UTC (rev 233412)
@@ -133,6 +133,7 @@
 		E132AA3D17CE776F00611DF0 /* WebKitTestRunnerEvent.mm in Sources */ = {isa = PBXBuildFile; fileRef = E132AA3B17CE776F00611DF0 /* WebKitTestRunnerEvent.mm */; };
 		E1C642C317CBCC7300D66A3C /* PoseAsClass.mm in Sources */ = {isa = PBXBuildFile; fileRef = E1C642C117CBCC7300D66A3C /* PoseAsClass.mm */; };
 		E1C642C617CBCD4C00D66A3C /* WebKitTestRunnerPasteboard.mm in Sources */ = {isa = PBXBuildFile; fileRef = E1C642C417CBCD4C00D66A3C /* WebKitTestRunnerPasteboard.mm */; };
+		F4C3578C20E8444600FA0748 /* LayoutTestSpellChecker.mm in Sources */ = {isa = PBXBuildFile; fileRef = F4C3578A20E8444000FA0748 /* LayoutTestSpellChecker.mm */; };
 /* End PBXBuildFile section */
 
 /* Begin PBXContainerItemProxy section */
@@ -372,6 +373,8 @@
 		E1C642C217CBCC7300D66A3C /* PoseAsClass.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PoseAsClass.h; sourceTree = "<group>"; };
 		E1C642C417CBCD4C00D66A3C /* WebKitTestRunnerPasteboard.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = WebKitTestRunnerPasteboard.mm; sourceTree = "<group>"; };
 		E1C642C517CBCD4C00D66A3C /* WebKitTestRunnerPasteboard.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WebKitTestRunnerPasteboard.h; sourceTree = "<group>"; };
+		F4C3578A20E8444000FA0748 /* LayoutTestSpellChecker.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = LayoutTestSpellChecker.mm; path = ../TestRunnerShared/cocoa/LayoutTestSpellChecker.mm; sourceTree = "<group>"; };
+		F4C3578B20E8444000FA0748 /* LayoutTestSpellChecker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = LayoutTestSpellChecker.h; path = ../TestRunnerShared/cocoa/LayoutTestSpellChecker.h; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
@@ -479,6 +482,7 @@
 			isa = PBXGroup;
 			children = (
 				0F18E71B1D6BC4E60027E547 /* Bindings */,
+				F4B6C31620E84369008AC225 /* cocoa */,
 				0F73B5471BA782FE004B3EF4 /* UIScriptContext */,
 				3148A0531E6F85B600D3B316 /* IOSLayoutTestCommunication.cpp */,
 				3148A0541E6F85B600D3B316 /* IOSLayoutTestCommunication.h */,
@@ -822,6 +826,15 @@
 			name = Shared;
 			sourceTree = "<group>";
 		};
+		F4B6C31620E84369008AC225 /* cocoa */ = {
+			isa = PBXGroup;
+			children = (
+				F4C3578B20E8444000FA0748 /* LayoutTestSpellChecker.h */,
+				F4C3578A20E8444000FA0748 /* LayoutTestSpellChecker.mm */,
+			);
+			name = cocoa;
+			sourceTree = "<group>";
+		};
 /* End PBXGroup section */
 
 /* Begin PBXHeadersBuildPhase section */
@@ -1046,6 +1059,7 @@
 				31DA8A3D1E7205CC00E1DF2F /* IOSLayoutTestCommunication.cpp in Sources */,
 				0F73B5511BA78968004B3EF4 /* JSUIScriptController.cpp in Sources */,
 				0F18E7181D6BC4560027E547 /* JSWrapper.cpp in Sources */,
+				F4C3578C20E8444600FA0748 /* LayoutTestSpellChecker.mm in Sources */,
 				A185103C1B9AE0FE00744AEB /* Options.cpp in Sources */,
 				A18510401B9AE13100744AEB /* PixelDumpSupport.cpp in Sources */,
 				2DFA98491D7F70CF00AFF2C9 /* SharedEventStreamsMac.mm in Sources */,
_______________________________________________
webkit-changes mailing list
[email protected]
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to