export const extractTextItems = async ( getPage: () => Promise ): Promise => { const page = await getPage(); let textContent = await page.getTextContent({ normalizeWhitespace: true, }); textContent = textContent.items; const strBuf = []; for (let j = 0, len = textContent.length; j < len; j += 1) { // add whitespace in front if start character is Uppercase strBuf.push(textContent[j].str); // if ( // textContent[j].str.match(/^[A-Z]/) && // j > 0 && // textContent[j - 1].str !== ' ' // ) { // strBuf.push(` ${textContent[j].str}`); // } else { // strBuf.push(textContent[j].str); // } } return strBuf; }; export const convertMatches = ( queryString: string, matchIndex: number, textContentItem: any[] ): Record => { let i = 0; let iIndex = 0; const end = textContentItem.length - 1; const queryLen = queryString.length; // Loop over the divIdxs. while (i !== end && matchIndex >= iIndex + textContentItem[i].length) { iIndex += textContentItem[i].length; i += 1; } if (i === textContentItem.length) { console.error('Could not find a matching mapping'); } const match: Record = { begin: { divIdx: i, offset: matchIndex - iIndex, }, }; // Calculate the end position. // eslint-disable-next-line no-param-reassign matchIndex += queryLen; // Somewhat the same array as above, but use > instead of >= to get // the end position right. while (i !== end && matchIndex > iIndex + textContentItem[i].length) { iIndex += textContentItem[i].length; i += 1; } match.end = { divIdx: i, offset: matchIndex - iIndex, }; return match; }; const appendTextToDiv = async ( domElements: HTMLElement[], getPage: () => Promise, divIdx: number, fromOffset: number, toOffset: number | undefined, highlight: boolean, id: string, queryStr = '' ): Promise => { const textContentItem = await extractTextItems(getPage); const domElement = domElements[divIdx]; const content = textContentItem[divIdx].substring(fromOffset, toOffset); const node = document.createTextNode( queryStr ? content.replace(queryStr, '') : content ); const span = document.createElement('span'); if (highlight) { span.setAttribute('class', id); span.style.backgroundColor = 'rgba(255, 211, 0, 0.7)'; span.appendChild(node); domElement.appendChild(span); } else { domElement.appendChild(node); } }; const beginText = async ( domElements: HTMLElement[], getPage: () => Promise, begin: Record, queryStr: string ): Promise => { const { divIdx } = begin; const domElement = domElements[divIdx]; if (domElement) { // eslint-disable-next-line no-param-reassign domElement.textContent = ''; appendTextToDiv( domElements, getPage, divIdx, 0, begin.offset, false, '', queryStr ); } }; export const renderMatches = async ( domElements: HTMLElement[], getPage: () => Promise, matchIndex: number, queryStr: string, id: string ): Promise => { const textContentItem = await extractTextItems(getPage); const { begin, end } = convertMatches(queryStr, matchIndex, textContentItem); beginText(domElements, getPage, begin, queryStr); if (begin.divIdx === end.divIdx) { appendTextToDiv( domElements, getPage, begin.divIdx, begin.offset, end.offset, true, id ); } else { for (let i = begin.divIdx; i <= end.divIdx; i += 1) { switch (i) { case begin.divIdx: appendTextToDiv( domElements, getPage, begin.divIdx, begin.offset, undefined, true, id ); break; case end.divIdx: beginText( domElements, getPage, { divIdx: end.divIdx, offset: 0 }, queryStr ); appendTextToDiv( domElements, getPage, end.divIdx, 0, end.offset, true, id ); break; default: { beginText(domElements, getPage, { divIdx: i, offset: 0 }, queryStr); appendTextToDiv(domElements, getPage, i, 0, undefined, true, id); break; } } } } };