Bug 1901466: Implement ITextRangeProvider::FindText, r=Jamie
This revision implements ITextRangeProvider::FindText on UiaTextRange. The function searches for a given string in the text range, forwards or backwards, case-sensitive or not. The algorithm to do so uses a single pass to build the range's text string plus an acceleration structure for lookup later. It then calls Find (or RFind) on the maybe-lowercased string before using that resulting index to binary search the acceleration structure for the proper start and end indices of the search string. Once it has those associated Accessibles, it builds a text range and returns it. This revision also implements tests for this functionality. Differential Revision: https://phabricator.services.mozilla.com/D236304
This commit is contained in:
@@ -2288,3 +2288,85 @@ line7</textarea>
|
|||||||
// The IA2 -> UIA proxy doesn't support GetVisibleRanges.
|
// The IA2 -> UIA proxy doesn't support GetVisibleRanges.
|
||||||
{ uiaEnabled: true, uiaDisabled: false }
|
{ uiaEnabled: true, uiaDisabled: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the TextRange pattern's FindText method.
|
||||||
|
*/
|
||||||
|
addUiaTask(
|
||||||
|
`<div id="container"><b>abc</b>TEST<div id="inner">def</div>TEST<p>ghi</p></div>`,
|
||||||
|
async function testTextRangeFromChild() {
|
||||||
|
await runPython(`
|
||||||
|
global doc, docText, container, range
|
||||||
|
doc = getDocUia()
|
||||||
|
docText = getUiaPattern(doc, "Text")
|
||||||
|
container = findUiaByDomId(doc, "container")
|
||||||
|
range = docText.RangeFromChild(container)
|
||||||
|
`);
|
||||||
|
// The IA2 -> UIA bridge inserts a space at the end of the text.
|
||||||
|
if (gIsUiaEnabled) {
|
||||||
|
is(
|
||||||
|
await runPython(`range.GetText(-1)`),
|
||||||
|
`abcTESTdefTESTghi`,
|
||||||
|
"doc returned correct range for container"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
info("Finding 'abc', searching from the start");
|
||||||
|
await runPython(`
|
||||||
|
global subrange
|
||||||
|
subrange = range.FindText("abc", False, False)
|
||||||
|
`);
|
||||||
|
is(await runPython(`subrange.GetText(-1)`), "abc", "range text correct");
|
||||||
|
|
||||||
|
info("Finding 'abc', searching from the end");
|
||||||
|
await runPython(`
|
||||||
|
global subrange
|
||||||
|
subrange = range.FindText("abc", True, False)
|
||||||
|
`);
|
||||||
|
is(await runPython(`subrange.GetText(-1)`), "abc", "range text correct");
|
||||||
|
|
||||||
|
info("Finding 'ghi', searching from the start");
|
||||||
|
await runPython(`
|
||||||
|
global subrange
|
||||||
|
subrange = range.FindText("ghi", False, False)
|
||||||
|
`);
|
||||||
|
is(await runPython(`subrange.GetText(-1)`), "ghi", "range text correct");
|
||||||
|
|
||||||
|
info("Finding 'ghi', searching from the end");
|
||||||
|
await runPython(`
|
||||||
|
global subrange
|
||||||
|
subrange = range.FindText("ghi", True, False)
|
||||||
|
`);
|
||||||
|
is(await runPython(`subrange.GetText(-1)`), "ghi", "range text correct");
|
||||||
|
|
||||||
|
info("Finding 'TEST', searching from the start");
|
||||||
|
await runPython(`
|
||||||
|
global subrange
|
||||||
|
subrange = range.FindText("TEST", False, False)
|
||||||
|
`);
|
||||||
|
is(await runPython(`subrange.GetText(-1)`), "TEST", "range text correct");
|
||||||
|
info("Finding 'TEST', searching from the end");
|
||||||
|
await runPython(`
|
||||||
|
global subrange2
|
||||||
|
subrange2 = range.FindText("TEST", True, False)
|
||||||
|
`);
|
||||||
|
is(await runPython(`subrange2.GetText(-1)`), "TEST", "range text correct");
|
||||||
|
ok(
|
||||||
|
!(await runPython(`subrange.compare(subrange2)`)),
|
||||||
|
"ranges are not equal"
|
||||||
|
);
|
||||||
|
|
||||||
|
info("Finding 'test', searching from the start, case-sensitive");
|
||||||
|
await runPython(`
|
||||||
|
global subrange
|
||||||
|
subrange = range.FindText("test", False, False)
|
||||||
|
`);
|
||||||
|
ok(await runPython(`not subrange`), "range not found");
|
||||||
|
info("Finding 'test', searching from the start, case-insensitive");
|
||||||
|
await runPython(`
|
||||||
|
global subrange
|
||||||
|
subrange = range.FindText("test", False, True)
|
||||||
|
`);
|
||||||
|
is(await runPython(`subrange.GetText(-1)`), "TEST", "range text correct");
|
||||||
|
},
|
||||||
|
{ uiaEnabled: true, uiaDisabled: true }
|
||||||
|
);
|
||||||
|
|||||||
@@ -463,7 +463,82 @@ UiaTextRange::FindAttribute(TEXTATTRIBUTEID aAttributeId, VARIANT aVal,
|
|||||||
STDMETHODIMP
|
STDMETHODIMP
|
||||||
UiaTextRange::FindText(__RPC__in BSTR aText, BOOL aBackward, BOOL aIgnoreCase,
|
UiaTextRange::FindText(__RPC__in BSTR aText, BOOL aBackward, BOOL aIgnoreCase,
|
||||||
__RPC__deref_out_opt ITextRangeProvider** aRetVal) {
|
__RPC__deref_out_opt ITextRangeProvider** aRetVal) {
|
||||||
return E_NOTIMPL;
|
if (!aRetVal) {
|
||||||
|
return E_INVALIDARG;
|
||||||
|
}
|
||||||
|
*aRetVal = nullptr;
|
||||||
|
TextLeafRange range = GetRange();
|
||||||
|
if (!range) {
|
||||||
|
return CO_E_OBJNOTCONNECTED;
|
||||||
|
}
|
||||||
|
MOZ_ASSERT(range.Start() <= range.End(), "Range must be valid to proceed.");
|
||||||
|
|
||||||
|
// We can't find anything in an empty range.
|
||||||
|
if (range.Start() == range.End()) {
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate over the range's leaf segments and append each leaf's text. Keep
|
||||||
|
// track of the indices in the built string, associating them with the
|
||||||
|
// Accessible pointer whose text begins at that index.
|
||||||
|
nsTArray<std::pair<int32_t, Accessible*>> indexToAcc;
|
||||||
|
nsAutoString rangeText;
|
||||||
|
for (const TextLeafRange leafSegment : range) {
|
||||||
|
Accessible* startAcc = leafSegment.Start().mAcc;
|
||||||
|
MOZ_ASSERT(startAcc, "Start acc of leaf segment was unexpectedly null.");
|
||||||
|
indexToAcc.EmplaceBack(rangeText.Length(), startAcc);
|
||||||
|
startAcc->AppendTextTo(rangeText);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the search string's start position in the text of the range, ignoring
|
||||||
|
// case if requested.
|
||||||
|
const nsDependentString searchStr{aText};
|
||||||
|
const int32_t startIndex = [&]() {
|
||||||
|
if (aIgnoreCase) {
|
||||||
|
ToLowerCase(rangeText);
|
||||||
|
nsAutoString searchStrLower;
|
||||||
|
ToLowerCase(searchStr, searchStrLower);
|
||||||
|
return aBackward ? rangeText.RFind(searchStrLower)
|
||||||
|
: rangeText.Find(searchStrLower);
|
||||||
|
} else {
|
||||||
|
return aBackward ? rangeText.RFind(searchStr) : rangeText.Find(searchStr);
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
if (startIndex == kNotFound) {
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
const int32_t endIndex = startIndex + searchStr.Length();
|
||||||
|
|
||||||
|
// Binary search for the (index, Accessible*) pair where the index is as large
|
||||||
|
// as possible without exceeding the size of the search index. The associated
|
||||||
|
// Accessible* is the Accessible for the resulting TextLeafPoint.
|
||||||
|
auto GetNearestAccLessThanIndex = [&indexToAcc](int32_t aIndex) {
|
||||||
|
MOZ_ASSERT(aIndex >= 0, "Search index is less than 0.");
|
||||||
|
auto itr =
|
||||||
|
std::lower_bound(indexToAcc.begin(), indexToAcc.end(), aIndex,
|
||||||
|
[](const std::pair<int32_t, Accessible*>& aPair,
|
||||||
|
int32_t aIndex) { return aPair.first <= aIndex; });
|
||||||
|
MOZ_ASSERT(itr != indexToAcc.begin(),
|
||||||
|
"Iterator is unexpectedly at the beginning.");
|
||||||
|
--itr;
|
||||||
|
return itr;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate the TextLeafPoint for the start and end of the found text.
|
||||||
|
auto itr = GetNearestAccLessThanIndex(startIndex);
|
||||||
|
Accessible* foundTextStart = itr->second;
|
||||||
|
const int32_t offsetFromStart = startIndex - itr->first;
|
||||||
|
const TextLeafPoint rangeStart{foundTextStart, offsetFromStart};
|
||||||
|
|
||||||
|
itr = GetNearestAccLessThanIndex(endIndex);
|
||||||
|
Accessible* foundTextEnd = itr->second;
|
||||||
|
const int32_t offsetFromEndAccStart = endIndex - itr->first;
|
||||||
|
const TextLeafPoint rangeEnd{foundTextEnd, offsetFromEndAccStart};
|
||||||
|
|
||||||
|
TextLeafRange resultRange{rangeStart, rangeEnd};
|
||||||
|
RefPtr uiaRange = new UiaTextRange(resultRange);
|
||||||
|
uiaRange.forget(aRetVal);
|
||||||
|
return S_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
template <TEXTATTRIBUTEID Attr>
|
template <TEXTATTRIBUTEID Attr>
|
||||||
|
|||||||
Reference in New Issue
Block a user