| /** | |
| * @licstart The following is the entire license notice for the | |
| * Javascript code in this page | |
| * | |
| * Copyright 2020 Mozilla Foundation | |
| * | |
| * Licensed under the Apache License, Version 2.0 (the "License"); | |
| * you may not use this file except in compliance with the License. | |
| * You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| * | |
| * @licend The above is the entire license notice for the | |
| * Javascript code in this page | |
| */ | |
| "use strict"; | |
| Object.defineProperty(exports, "__esModule", { | |
| value: true | |
| }); | |
| exports.PDFFindController = exports.FindState = void 0; | |
| var _pdf = require("../pdf"); | |
| var _pdf_find_utils = require("./pdf_find_utils.js"); | |
| var _ui_utils = require("./ui_utils.js"); | |
| const FindState = { | |
| FOUND: 0, | |
| NOT_FOUND: 1, | |
| WRAPPED: 2, | |
| PENDING: 3 | |
| }; | |
| exports.FindState = FindState; | |
| const FIND_TIMEOUT = 250; | |
| const MATCH_SCROLL_OFFSET_TOP = -50; | |
| const MATCH_SCROLL_OFFSET_LEFT = -400; | |
| const CHARACTERS_TO_NORMALIZE = { | |
| "\u2018": "'", | |
| "\u2019": "'", | |
| "\u201A": "'", | |
| "\u201B": "'", | |
| "\u201C": '"', | |
| "\u201D": '"', | |
| "\u201E": '"', | |
| "\u201F": '"', | |
| "\u00BC": "1/4", | |
| "\u00BD": "1/2", | |
| "\u00BE": "3/4" | |
| }; | |
| let normalizationRegex = null; | |
| function normalize(text) { | |
| if (!normalizationRegex) { | |
| const replace = Object.keys(CHARACTERS_TO_NORMALIZE).join(""); | |
| normalizationRegex = new RegExp(`[${replace}]`, "g"); | |
| } | |
| return text.replace(normalizationRegex, function (ch) { | |
| return CHARACTERS_TO_NORMALIZE[ch]; | |
| }); | |
| } | |
| class PDFFindController { | |
| constructor({ | |
| linkService, | |
| eventBus | |
| }) { | |
| this._linkService = linkService; | |
| this._eventBus = eventBus; | |
| this._reset(); | |
| eventBus._on("findbarclose", this._onFindBarClose.bind(this)); | |
| } | |
| get highlightMatches() { | |
| return this._highlightMatches; | |
| } | |
| get pageMatches() { | |
| return this._pageMatches; | |
| } | |
| get pageMatchesLength() { | |
| return this._pageMatchesLength; | |
| } | |
| get selected() { | |
| return this._selected; | |
| } | |
| get state() { | |
| return this._state; | |
| } | |
| setDocument(pdfDocument) { | |
| if (this._pdfDocument) { | |
| this._reset(); | |
| } | |
| if (!pdfDocument) { | |
| return; | |
| } | |
| this._pdfDocument = pdfDocument; | |
| this._firstPageCapability.resolve(); | |
| } | |
| executeCommand(cmd, state) { | |
| if (!state) { | |
| return; | |
| } | |
| const pdfDocument = this._pdfDocument; | |
| if (this._state === null || this._shouldDirtyMatch(cmd, state)) { | |
| this._dirtyMatch = true; | |
| } | |
| this._state = state; | |
| if (cmd !== "findhighlightallchange") { | |
| this._updateUIState(FindState.PENDING); | |
| } | |
| this._firstPageCapability.promise.then(() => { | |
| if (!this._pdfDocument || pdfDocument && this._pdfDocument !== pdfDocument) { | |
| return; | |
| } | |
| this._extractText(); | |
| const findbarClosed = !this._highlightMatches; | |
| const pendingTimeout = !!this._findTimeout; | |
| if (this._findTimeout) { | |
| clearTimeout(this._findTimeout); | |
| this._findTimeout = null; | |
| } | |
| if (cmd === "find") { | |
| this._findTimeout = setTimeout(() => { | |
| this._nextMatch(); | |
| this._findTimeout = null; | |
| }, FIND_TIMEOUT); | |
| } else if (this._dirtyMatch) { | |
| this._nextMatch(); | |
| } else if (cmd === "findagain") { | |
| this._nextMatch(); | |
| if (findbarClosed && this._state.highlightAll) { | |
| this._updateAllPages(); | |
| } | |
| } else if (cmd === "findhighlightallchange") { | |
| if (pendingTimeout) { | |
| this._nextMatch(); | |
| } else { | |
| this._highlightMatches = true; | |
| } | |
| this._updateAllPages(); | |
| } else { | |
| this._nextMatch(); | |
| } | |
| }); | |
| } | |
| scrollMatchIntoView({ | |
| element = null, | |
| pageIndex = -1, | |
| matchIndex = -1 | |
| }) { | |
| if (!this._scrollMatches || !element) { | |
| return; | |
| } else if (matchIndex === -1 || matchIndex !== this._selected.matchIdx) { | |
| return; | |
| } else if (pageIndex === -1 || pageIndex !== this._selected.pageIdx) { | |
| return; | |
| } | |
| this._scrollMatches = false; | |
| const spot = { | |
| top: MATCH_SCROLL_OFFSET_TOP, | |
| left: MATCH_SCROLL_OFFSET_LEFT | |
| }; | |
| (0, _ui_utils.scrollIntoView)(element, spot, true); | |
| } | |
| _reset() { | |
| this._highlightMatches = false; | |
| this._scrollMatches = false; | |
| this._pdfDocument = null; | |
| this._pageMatches = []; | |
| this._pageMatchesLength = []; | |
| this._state = null; | |
| this._selected = { | |
| pageIdx: -1, | |
| matchIdx: -1 | |
| }; | |
| this._offset = { | |
| pageIdx: null, | |
| matchIdx: null, | |
| wrapped: false | |
| }; | |
| this._extractTextPromises = []; | |
| this._pageContents = []; | |
| this._matchesCountTotal = 0; | |
| this._pagesToSearch = null; | |
| this._pendingFindMatches = Object.create(null); | |
| this._resumePageIdx = null; | |
| this._dirtyMatch = false; | |
| clearTimeout(this._findTimeout); | |
| this._findTimeout = null; | |
| this._firstPageCapability = (0, _pdf.createPromiseCapability)(); | |
| } | |
| get _query() { | |
| if (this._state.query !== this._rawQuery) { | |
| this._rawQuery = this._state.query; | |
| this._normalizedQuery = normalize(this._state.query); | |
| } | |
| return this._normalizedQuery; | |
| } | |
| _shouldDirtyMatch(cmd, state) { | |
| if (state.query !== this._state.query) { | |
| return true; | |
| } | |
| switch (cmd) { | |
| case "findagain": | |
| const pageNumber = this._selected.pageIdx + 1; | |
| const linkService = this._linkService; | |
| if (pageNumber >= 1 && pageNumber <= linkService.pagesCount && pageNumber !== linkService.page && !linkService.isPageVisible(pageNumber)) { | |
| return true; | |
| } | |
| return false; | |
| case "findhighlightallchange": | |
| return false; | |
| } | |
| return true; | |
| } | |
| _prepareMatches(matchesWithLength, matches, matchesLength) { | |
| function isSubTerm(currentIndex) { | |
| const currentElem = matchesWithLength[currentIndex]; | |
| const nextElem = matchesWithLength[currentIndex + 1]; | |
| if (currentIndex < matchesWithLength.length - 1 && currentElem.match === nextElem.match) { | |
| currentElem.skipped = true; | |
| return true; | |
| } | |
| for (let i = currentIndex - 1; i >= 0; i--) { | |
| const prevElem = matchesWithLength[i]; | |
| if (prevElem.skipped) { | |
| continue; | |
| } | |
| if (prevElem.match + prevElem.matchLength < currentElem.match) { | |
| break; | |
| } | |
| if (prevElem.match + prevElem.matchLength >= currentElem.match + currentElem.matchLength) { | |
| currentElem.skipped = true; | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| matchesWithLength.sort(function (a, b) { | |
| return a.match === b.match ? a.matchLength - b.matchLength : a.match - b.match; | |
| }); | |
| for (let i = 0, len = matchesWithLength.length; i < len; i++) { | |
| if (isSubTerm(i)) { | |
| continue; | |
| } | |
| matches.push(matchesWithLength[i].match); | |
| matchesLength.push(matchesWithLength[i].matchLength); | |
| } | |
| } | |
| _isEntireWord(content, startIdx, length) { | |
| if (startIdx > 0) { | |
| const first = content.charCodeAt(startIdx); | |
| const limit = content.charCodeAt(startIdx - 1); | |
| if ((0, _pdf_find_utils.getCharacterType)(first) === (0, _pdf_find_utils.getCharacterType)(limit)) { | |
| return false; | |
| } | |
| } | |
| const endIdx = startIdx + length - 1; | |
| if (endIdx < content.length - 1) { | |
| const last = content.charCodeAt(endIdx); | |
| const limit = content.charCodeAt(endIdx + 1); | |
| if ((0, _pdf_find_utils.getCharacterType)(last) === (0, _pdf_find_utils.getCharacterType)(limit)) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| _calculatePhraseMatch(query, pageIndex, pageContent, entireWord) { | |
| const matches = []; | |
| const queryLen = query.length; | |
| let matchIdx = -queryLen; | |
| while (true) { | |
| matchIdx = pageContent.indexOf(query, matchIdx + queryLen); | |
| if (matchIdx === -1) { | |
| break; | |
| } | |
| if (entireWord && !this._isEntireWord(pageContent, matchIdx, queryLen)) { | |
| continue; | |
| } | |
| matches.push(matchIdx); | |
| } | |
| this._pageMatches[pageIndex] = matches; | |
| } | |
| _calculateWordMatch(query, pageIndex, pageContent, entireWord) { | |
| const matchesWithLength = []; | |
| const queryArray = query.match(/\S+/g); | |
| for (let i = 0, len = queryArray.length; i < len; i++) { | |
| const subquery = queryArray[i]; | |
| const subqueryLen = subquery.length; | |
| let matchIdx = -subqueryLen; | |
| while (true) { | |
| matchIdx = pageContent.indexOf(subquery, matchIdx + subqueryLen); | |
| if (matchIdx === -1) { | |
| break; | |
| } | |
| if (entireWord && !this._isEntireWord(pageContent, matchIdx, subqueryLen)) { | |
| continue; | |
| } | |
| matchesWithLength.push({ | |
| match: matchIdx, | |
| matchLength: subqueryLen, | |
| skipped: false | |
| }); | |
| } | |
| } | |
| this._pageMatchesLength[pageIndex] = []; | |
| this._pageMatches[pageIndex] = []; | |
| this._prepareMatches(matchesWithLength, this._pageMatches[pageIndex], this._pageMatchesLength[pageIndex]); | |
| } | |
| _calculateMatch(pageIndex) { | |
| let pageContent = this._pageContents[pageIndex]; | |
| let query = this._query; | |
| const { | |
| caseSensitive, | |
| entireWord, | |
| phraseSearch | |
| } = this._state; | |
| if (query.length === 0) { | |
| return; | |
| } | |
| if (!caseSensitive) { | |
| pageContent = pageContent.toLowerCase(); | |
| query = query.toLowerCase(); | |
| } | |
| if (phraseSearch) { | |
| this._calculatePhraseMatch(query, pageIndex, pageContent, entireWord); | |
| } else { | |
| this._calculateWordMatch(query, pageIndex, pageContent, entireWord); | |
| } | |
| if (this._state.highlightAll) { | |
| this._updatePage(pageIndex); | |
| } | |
| if (this._resumePageIdx === pageIndex) { | |
| this._resumePageIdx = null; | |
| this._nextPageMatch(); | |
| } | |
| const pageMatchesCount = this._pageMatches[pageIndex].length; | |
| if (pageMatchesCount > 0) { | |
| this._matchesCountTotal += pageMatchesCount; | |
| this._updateUIResultsCount(); | |
| } | |
| } | |
| _extractText() { | |
| if (this._extractTextPromises.length > 0) { | |
| return; | |
| } | |
| let promise = Promise.resolve(); | |
| for (let i = 0, ii = this._linkService.pagesCount; i < ii; i++) { | |
| const extractTextCapability = (0, _pdf.createPromiseCapability)(); | |
| this._extractTextPromises[i] = extractTextCapability.promise; | |
| promise = promise.then(() => { | |
| return this._pdfDocument.getPage(i + 1).then(pdfPage => { | |
| return pdfPage.getTextContent({ | |
| normalizeWhitespace: true | |
| }); | |
| }).then(textContent => { | |
| const textItems = textContent.items; | |
| const strBuf = []; | |
| for (let j = 0, jj = textItems.length; j < jj; j++) { | |
| strBuf.push(textItems[j].str); | |
| } | |
| this._pageContents[i] = normalize(strBuf.join("")); | |
| extractTextCapability.resolve(i); | |
| }, reason => { | |
| console.error(`Unable to get text content for page ${i + 1}`, reason); | |
| this._pageContents[i] = ""; | |
| extractTextCapability.resolve(i); | |
| }); | |
| }); | |
| } | |
| } | |
| _updatePage(index) { | |
| if (this._scrollMatches && this._selected.pageIdx === index) { | |
| this._linkService.page = index + 1; | |
| } | |
| this._eventBus.dispatch("updatetextlayermatches", { | |
| source: this, | |
| pageIndex: index | |
| }); | |
| } | |
| _updateAllPages() { | |
| this._eventBus.dispatch("updatetextlayermatches", { | |
| source: this, | |
| pageIndex: -1 | |
| }); | |
| } | |
| _nextMatch() { | |
| const previous = this._state.findPrevious; | |
| const currentPageIndex = this._linkService.page - 1; | |
| const numPages = this._linkService.pagesCount; | |
| this._highlightMatches = true; | |
| if (this._dirtyMatch) { | |
| this._dirtyMatch = false; | |
| this._selected.pageIdx = this._selected.matchIdx = -1; | |
| this._offset.pageIdx = currentPageIndex; | |
| this._offset.matchIdx = null; | |
| this._offset.wrapped = false; | |
| this._resumePageIdx = null; | |
| this._pageMatches.length = 0; | |
| this._pageMatchesLength.length = 0; | |
| this._matchesCountTotal = 0; | |
| this._updateAllPages(); | |
| for (let i = 0; i < numPages; i++) { | |
| if (this._pendingFindMatches[i] === true) { | |
| continue; | |
| } | |
| this._pendingFindMatches[i] = true; | |
| this._extractTextPromises[i].then(pageIdx => { | |
| delete this._pendingFindMatches[pageIdx]; | |
| this._calculateMatch(pageIdx); | |
| }); | |
| } | |
| } | |
| if (this._query === "") { | |
| this._updateUIState(FindState.FOUND); | |
| return; | |
| } | |
| if (this._resumePageIdx) { | |
| return; | |
| } | |
| const offset = this._offset; | |
| this._pagesToSearch = numPages; | |
| if (offset.matchIdx !== null) { | |
| const numPageMatches = this._pageMatches[offset.pageIdx].length; | |
| if (!previous && offset.matchIdx + 1 < numPageMatches || previous && offset.matchIdx > 0) { | |
| offset.matchIdx = previous ? offset.matchIdx - 1 : offset.matchIdx + 1; | |
| this._updateMatch(true); | |
| return; | |
| } | |
| this._advanceOffsetPage(previous); | |
| } | |
| this._nextPageMatch(); | |
| } | |
| _matchesReady(matches) { | |
| const offset = this._offset; | |
| const numMatches = matches.length; | |
| const previous = this._state.findPrevious; | |
| if (numMatches) { | |
| offset.matchIdx = previous ? numMatches - 1 : 0; | |
| this._updateMatch(true); | |
| return true; | |
| } | |
| this._advanceOffsetPage(previous); | |
| if (offset.wrapped) { | |
| offset.matchIdx = null; | |
| if (this._pagesToSearch < 0) { | |
| this._updateMatch(false); | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| _nextPageMatch() { | |
| if (this._resumePageIdx !== null) { | |
| console.error("There can only be one pending page."); | |
| } | |
| let matches = null; | |
| do { | |
| const pageIdx = this._offset.pageIdx; | |
| matches = this._pageMatches[pageIdx]; | |
| if (!matches) { | |
| this._resumePageIdx = pageIdx; | |
| break; | |
| } | |
| } while (!this._matchesReady(matches)); | |
| } | |
| _advanceOffsetPage(previous) { | |
| const offset = this._offset; | |
| const numPages = this._linkService.pagesCount; | |
| offset.pageIdx = previous ? offset.pageIdx - 1 : offset.pageIdx + 1; | |
| offset.matchIdx = null; | |
| this._pagesToSearch--; | |
| if (offset.pageIdx >= numPages || offset.pageIdx < 0) { | |
| offset.pageIdx = previous ? numPages - 1 : 0; | |
| offset.wrapped = true; | |
| } | |
| } | |
| _updateMatch(found = false) { | |
| let state = FindState.NOT_FOUND; | |
| const wrapped = this._offset.wrapped; | |
| this._offset.wrapped = false; | |
| if (found) { | |
| const previousPage = this._selected.pageIdx; | |
| this._selected.pageIdx = this._offset.pageIdx; | |
| this._selected.matchIdx = this._offset.matchIdx; | |
| state = wrapped ? FindState.WRAPPED : FindState.FOUND; | |
| if (previousPage !== -1 && previousPage !== this._selected.pageIdx) { | |
| this._updatePage(previousPage); | |
| } | |
| } | |
| this._updateUIState(state, this._state.findPrevious); | |
| if (this._selected.pageIdx !== -1) { | |
| this._scrollMatches = true; | |
| this._updatePage(this._selected.pageIdx); | |
| } | |
| } | |
| _onFindBarClose(evt) { | |
| const pdfDocument = this._pdfDocument; | |
| this._firstPageCapability.promise.then(() => { | |
| if (!this._pdfDocument || pdfDocument && this._pdfDocument !== pdfDocument) { | |
| return; | |
| } | |
| if (this._findTimeout) { | |
| clearTimeout(this._findTimeout); | |
| this._findTimeout = null; | |
| } | |
| if (this._resumePageIdx) { | |
| this._resumePageIdx = null; | |
| this._dirtyMatch = true; | |
| } | |
| this._updateUIState(FindState.FOUND); | |
| this._highlightMatches = false; | |
| this._updateAllPages(); | |
| }); | |
| } | |
| _requestMatchesCount() { | |
| const { | |
| pageIdx, | |
| matchIdx | |
| } = this._selected; | |
| let current = 0, | |
| total = this._matchesCountTotal; | |
| if (matchIdx !== -1) { | |
| for (let i = 0; i < pageIdx; i++) { | |
| current += this._pageMatches[i] && this._pageMatches[i].length || 0; | |
| } | |
| current += matchIdx + 1; | |
| } | |
| if (current < 1 || current > total) { | |
| current = total = 0; | |
| } | |
| return { | |
| current, | |
| total | |
| }; | |
| } | |
| _updateUIResultsCount() { | |
| this._eventBus.dispatch("updatefindmatchescount", { | |
| source: this, | |
| matchesCount: this._requestMatchesCount() | |
| }); | |
| } | |
| _updateUIState(state, previous) { | |
| this._eventBus.dispatch("updatefindcontrolstate", { | |
| source: this, | |
| state, | |
| previous, | |
| matchesCount: this._requestMatchesCount(), | |
| rawQuery: this._state ? this._state.query : null | |
| }); | |
| } | |
| } | |
| exports.PDFFindController = PDFFindController; |