diff --git a/test/integration/autolinker_spec.mjs b/test/integration/autolinker_spec.mjs index 5a8321924a8f4..a7cbecd29c5f4 100644 --- a/test/integration/autolinker_spec.mjs +++ b/test/integration/autolinker_spec.mjs @@ -13,10 +13,46 @@ * limitations under the License. */ -import { closePages, createPromise, loadAndWait } from "./test_utils.mjs"; +import { + awaitPromise, + closePages, + createPromise, + loadAndWait, +} from "./test_utils.mjs"; -function waitForLinkAnnotations(page) { +function waitForLinkAnnotations(page, pageNumber) { + return page.evaluateHandle( + number => [ + new Promise(resolve => { + const { eventBus } = window.PDFViewerApplication; + eventBus.on("linkannotationsadded", function listener(e) { + if (number === undefined || e.pageNumber === number) { + resolve(); + eventBus.off("linkannotationsadded", listener); + } + }); + }), + ], + pageNumber + ); +} + +function recordInitialLinkAnnotationsEvent(eventBus) { + globalThis.initialLinkAnnotationsEventFired = false; + eventBus.on( + "linkannotationsadded", + () => { + globalThis.initialLinkAnnotationsEventFired = true; + }, + { once: true } + ); +} +function waitForInitialLinkAnnotations(page) { return createPromise(page, resolve => { + if (globalThis.initialLinkAnnotationsEventFired) { + resolve(); + return; + } window.PDFViewerApplication.eventBus.on("linkannotationsadded", resolve, { once: true, }); @@ -178,4 +214,49 @@ describe("autolinker", function () { ); }); }); + + describe("when highlighting search results", function () { + let pages; + + beforeAll(async () => { + pages = await loadAndWait( + "issue3115r.pdf", + ".annotationLayer", + null, + { eventBusSetup: recordInitialLinkAnnotationsEvent }, + { enableAutoLinking: true } + ); + }); + + afterAll(async () => { + await closePages(pages); + }); + + it("must find links that overlap with search results", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await awaitPromise(await waitForInitialLinkAnnotations(page)); + + const linkAnnotationsPromise = await waitForLinkAnnotations(page, 36); + + // Search for "rich.edu" + await page.click("#viewFindButton"); + await page.waitForSelector("#viewFindButton", { hidden: false }); + await page.type("#findInput", "rich.edu"); + await page.waitForSelector(".textLayer .highlight"); + + await awaitPromise(linkAnnotationsPromise); + + const urls = await page.$$eval( + ".page[data-page-number='36'] > .annotationLayer > .linkAnnotation > a", + annotations => annotations.map(a => a.href) + ); + + expect(urls) + .withContext(`In ${browserName}`) + .toContain(jasmine.stringContaining("rich.edu")); + }) + ); + }); + }); }); diff --git a/web/autolinker.js b/web/autolinker.js index d3ee14c282265..fe28235c2bdd4 100644 --- a/web/autolinker.js +++ b/web/autolinker.js @@ -69,13 +69,55 @@ function calculateLinkPosition(range, pdfPageView) { return { quadPoints, rect }; } +/** + * Given a DOM node `container` and an index into its text contents `offset`, + * returns a pair consisting of text node that the `offset` actually points + * to, together with the offset relative to that text node. + * When the offset points at the boundary between two node, the result will + * point to the first text node in depth-first traversal order. + * + * For example, given this DOM: + *
abcdefghi
+ * + * textPosition(p, 0) -> [#text "abc", 0] (before `a`) + * textPosition(p, 2) -> [#text "abc", 2] (between `b` and `c`) + * textPosition(p, 3) -> [#text "abc", 3] (after `c`) + * textPosition(p, 5) -> [#text "def", 2] (between `e` and `f`) + * textPosition(p, 6) -> [#text "def", 3] (after `f`) + */ +function textPosition(container, offset) { + let currentContainer = container; + do { + if (currentContainer.nodeType === Node.TEXT_NODE) { + const currentLength = currentContainer.textContent.length; + if (offset <= currentLength) { + return [currentContainer, offset]; + } + offset -= currentLength; + } else if (currentContainer.firstChild) { + currentContainer = currentContainer.firstChild; + continue; + } + + while (!currentContainer.nextSibling && currentContainer !== container) { + currentContainer = currentContainer.parentNode; + } + if (currentContainer !== container) { + currentContainer = currentContainer.nextSibling; + } + } while (currentContainer !== container); + throw new Error("Offset is bigger than container's contents length."); +} + function createLinkAnnotation({ url, index, length }, pdfPageView, id) { const highlighter = pdfPageView._textHighlighter; const [{ begin, end }] = highlighter._convertMatches([index], [length]); const range = new Range(); - range.setStart(highlighter.textDivs[begin.divIdx].firstChild, begin.offset); - range.setEnd(highlighter.textDivs[end.divIdx].firstChild, end.offset); + range.setStart( + ...textPosition(highlighter.textDivs[begin.divIdx], begin.offset) + ); + range.setEnd(...textPosition(highlighter.textDivs[end.divIdx], end.offset)); return { id: `inferred_link_${id}`,