Skip to content
This repository was archived by the owner on Jul 9, 2025. It is now read-only.

Commit 71db980

Browse files
author
Nathan LaPre
committed
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
1 parent 3c6a79f commit 71db980

File tree

2 files changed

+158
-1
lines changed

2 files changed

+158
-1
lines changed

accessible/tests/browser/windows/uia/browser_textPatterns.js

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2288,3 +2288,85 @@ line7</textarea>
22882288
// The IA2 -> UIA proxy doesn't support GetVisibleRanges.
22892289
{ uiaEnabled: true, uiaDisabled: false }
22902290
);
2291+
2292+
/**
2293+
* Test the TextRange pattern's FindText method.
2294+
*/
2295+
addUiaTask(
2296+
`<div id="container"><b>abc</b>TEST<div id="inner">def</div>TEST<p>ghi</p></div>`,
2297+
async function testTextRangeFromChild() {
2298+
await runPython(`
2299+
global doc, docText, container, range
2300+
doc = getDocUia()
2301+
docText = getUiaPattern(doc, "Text")
2302+
container = findUiaByDomId(doc, "container")
2303+
range = docText.RangeFromChild(container)
2304+
`);
2305+
// The IA2 -> UIA bridge inserts a space at the end of the text.
2306+
if (gIsUiaEnabled) {
2307+
is(
2308+
await runPython(`range.GetText(-1)`),
2309+
`abcTESTdefTESTghi`,
2310+
"doc returned correct range for container"
2311+
);
2312+
}
2313+
info("Finding 'abc', searching from the start");
2314+
await runPython(`
2315+
global subrange
2316+
subrange = range.FindText("abc", False, False)
2317+
`);
2318+
is(await runPython(`subrange.GetText(-1)`), "abc", "range text correct");
2319+
2320+
info("Finding 'abc', searching from the end");
2321+
await runPython(`
2322+
global subrange
2323+
subrange = range.FindText("abc", True, False)
2324+
`);
2325+
is(await runPython(`subrange.GetText(-1)`), "abc", "range text correct");
2326+
2327+
info("Finding 'ghi', searching from the start");
2328+
await runPython(`
2329+
global subrange
2330+
subrange = range.FindText("ghi", False, False)
2331+
`);
2332+
is(await runPython(`subrange.GetText(-1)`), "ghi", "range text correct");
2333+
2334+
info("Finding 'ghi', searching from the end");
2335+
await runPython(`
2336+
global subrange
2337+
subrange = range.FindText("ghi", True, False)
2338+
`);
2339+
is(await runPython(`subrange.GetText(-1)`), "ghi", "range text correct");
2340+
2341+
info("Finding 'TEST', searching from the start");
2342+
await runPython(`
2343+
global subrange
2344+
subrange = range.FindText("TEST", False, False)
2345+
`);
2346+
is(await runPython(`subrange.GetText(-1)`), "TEST", "range text correct");
2347+
info("Finding 'TEST', searching from the end");
2348+
await runPython(`
2349+
global subrange2
2350+
subrange2 = range.FindText("TEST", True, False)
2351+
`);
2352+
is(await runPython(`subrange2.GetText(-1)`), "TEST", "range text correct");
2353+
ok(
2354+
!(await runPython(`subrange.compare(subrange2)`)),
2355+
"ranges are not equal"
2356+
);
2357+
2358+
info("Finding 'test', searching from the start, case-sensitive");
2359+
await runPython(`
2360+
global subrange
2361+
subrange = range.FindText("test", False, False)
2362+
`);
2363+
ok(await runPython(`not subrange`), "range not found");
2364+
info("Finding 'test', searching from the start, case-insensitive");
2365+
await runPython(`
2366+
global subrange
2367+
subrange = range.FindText("test", False, True)
2368+
`);
2369+
is(await runPython(`subrange.GetText(-1)`), "TEST", "range text correct");
2370+
},
2371+
{ uiaEnabled: true, uiaDisabled: true }
2372+
);

accessible/windows/uia/UiaTextRange.cpp

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,82 @@ UiaTextRange::FindAttribute(TEXTATTRIBUTEID aAttributeId, VARIANT aVal,
463463
STDMETHODIMP
464464
UiaTextRange::FindText(__RPC__in BSTR aText, BOOL aBackward, BOOL aIgnoreCase,
465465
__RPC__deref_out_opt ITextRangeProvider** aRetVal) {
466-
return E_NOTIMPL;
466+
if (!aRetVal) {
467+
return E_INVALIDARG;
468+
}
469+
*aRetVal = nullptr;
470+
TextLeafRange range = GetRange();
471+
if (!range) {
472+
return CO_E_OBJNOTCONNECTED;
473+
}
474+
MOZ_ASSERT(range.Start() <= range.End(), "Range must be valid to proceed.");
475+
476+
// We can't find anything in an empty range.
477+
if (range.Start() == range.End()) {
478+
return S_OK;
479+
}
480+
481+
// Iterate over the range's leaf segments and append each leaf's text. Keep
482+
// track of the indices in the built string, associating them with the
483+
// Accessible pointer whose text begins at that index.
484+
nsTArray<std::pair<int32_t, Accessible*>> indexToAcc;
485+
nsAutoString rangeText;
486+
for (const TextLeafRange leafSegment : range) {
487+
Accessible* startAcc = leafSegment.Start().mAcc;
488+
MOZ_ASSERT(startAcc, "Start acc of leaf segment was unexpectedly null.");
489+
indexToAcc.EmplaceBack(rangeText.Length(), startAcc);
490+
startAcc->AppendTextTo(rangeText);
491+
}
492+
493+
// Find the search string's start position in the text of the range, ignoring
494+
// case if requested.
495+
const nsDependentString searchStr{aText};
496+
const int32_t startIndex = [&]() {
497+
if (aIgnoreCase) {
498+
ToLowerCase(rangeText);
499+
nsAutoString searchStrLower;
500+
ToLowerCase(searchStr, searchStrLower);
501+
return aBackward ? rangeText.RFind(searchStrLower)
502+
: rangeText.Find(searchStrLower);
503+
} else {
504+
return aBackward ? rangeText.RFind(searchStr) : rangeText.Find(searchStr);
505+
}
506+
}();
507+
if (startIndex == kNotFound) {
508+
return S_OK;
509+
}
510+
const int32_t endIndex = startIndex + searchStr.Length();
511+
512+
// Binary search for the (index, Accessible*) pair where the index is as large
513+
// as possible without exceeding the size of the search index. The associated
514+
// Accessible* is the Accessible for the resulting TextLeafPoint.
515+
auto GetNearestAccLessThanIndex = [&indexToAcc](int32_t aIndex) {
516+
MOZ_ASSERT(aIndex >= 0, "Search index is less than 0.");
517+
auto itr =
518+
std::lower_bound(indexToAcc.begin(), indexToAcc.end(), aIndex,
519+
[](const std::pair<int32_t, Accessible*>& aPair,
520+
int32_t aIndex) { return aPair.first <= aIndex; });
521+
MOZ_ASSERT(itr != indexToAcc.begin(),
522+
"Iterator is unexpectedly at the beginning.");
523+
--itr;
524+
return itr;
525+
};
526+
527+
// Calculate the TextLeafPoint for the start and end of the found text.
528+
auto itr = GetNearestAccLessThanIndex(startIndex);
529+
Accessible* foundTextStart = itr->second;
530+
const int32_t offsetFromStart = startIndex - itr->first;
531+
const TextLeafPoint rangeStart{foundTextStart, offsetFromStart};
532+
533+
itr = GetNearestAccLessThanIndex(endIndex);
534+
Accessible* foundTextEnd = itr->second;
535+
const int32_t offsetFromEndAccStart = endIndex - itr->first;
536+
const TextLeafPoint rangeEnd{foundTextEnd, offsetFromEndAccStart};
537+
538+
TextLeafRange resultRange{rangeStart, rangeEnd};
539+
RefPtr uiaRange = new UiaTextRange(resultRange);
540+
uiaRange.forget(aRetVal);
541+
return S_OK;
467542
}
468543

469544
template <TEXTATTRIBUTEID Attr>

0 commit comments

Comments
 (0)