diff --git a/web/app.js b/web/app.js index d61bdfdd9..22ed860d5 100644 --- a/web/app.js +++ b/web/app.js @@ -1251,6 +1251,7 @@ var PDFViewerApplication = { eventBus.on('rotateccw', webViewerRotateCcw); eventBus.on('documentproperties', webViewerDocumentProperties); eventBus.on('find', webViewerFind); + eventBus.on('findfromurlhash', webViewerFindFromUrlHash); //#if GENERIC eventBus.on('fileinputchange', webViewerFileInputChange); //#endif @@ -1906,12 +1907,23 @@ function webViewerDocumentProperties() { function webViewerFind(e) { PDFViewerApplication.findController.executeCommand('find' + e.type, { query: e.query, + phraseSearch: e.phraseSearch, caseSensitive: e.caseSensitive, highlightAll: e.highlightAll, findPrevious: e.findPrevious }); } +function webViewerFindFromUrlHash(e) { + PDFViewerApplication.findController.executeCommand('find', { + query: e.query, + phraseSearch: e.phraseSearch, + caseSensitive: false, + highlightAll: true, + findPrevious: false + }); +} + function webViewerScaleChanging(e) { var appConfig = PDFViewerApplication.appConfig; appConfig.toolbar.zoomOut.disabled = (e.scale === MIN_SCALE); @@ -2055,6 +2067,7 @@ window.addEventListener('keydown', function keydown(evt) { if (findState) { PDFViewerApplication.findController.executeCommand('findagain', { query: findState.query, + phraseSearch: findState.phraseSearch, caseSensitive: findState.caseSensitive, highlightAll: findState.highlightAll, findPrevious: cmd === 5 || cmd === 12 diff --git a/web/dom_events.js b/web/dom_events.js index ff048c87f..80893f52a 100644 --- a/web/dom_events.js +++ b/web/dom_events.js @@ -88,6 +88,7 @@ var event = document.createEvent('CustomEvent'); event.initCustomEvent('find' + e.type, true, true, { query: e.query, + phraseSearch: e.phraseSearch, caseSensitive: e.caseSensitive, highlightAll: e.highlightAll, findPrevious: e.findPrevious diff --git a/web/firefoxcom.js b/web/firefoxcom.js index 236a0b9ef..42bf997f2 100644 --- a/web/firefoxcom.js +++ b/web/firefoxcom.js @@ -163,6 +163,7 @@ Preferences._readFromStorage = function (prefObj) { source: window, type: evt.type.substring('find'.length), query: evt.detail.query, + phraseSearch: true, caseSensitive: !!evt.detail.caseSensitive, highlightAll: !!evt.detail.highlightAll, findPrevious: !!evt.detail.findPrevious diff --git a/web/pdf_find_bar.js b/web/pdf_find_bar.js index bdcd0a5cf..c01ec4761 100644 --- a/web/pdf_find_bar.js +++ b/web/pdf_find_bar.js @@ -109,6 +109,7 @@ var PDFFindBar = (function PDFFindBarClosure() { type: type, query: this.findField.value, caseSensitive: this.caseSensitive.checked, + phraseSearch: true, highlightAll: this.highlightAll.checked, findPrevious: findPrev }); diff --git a/web/pdf_find_controller.js b/web/pdf_find_controller.js index b085a6bc5..7d7a27325 100644 --- a/web/pdf_find_controller.js +++ b/web/pdf_find_controller.js @@ -78,6 +78,7 @@ var PDFFindController = (function PDFFindControllerClosure() { this.active = false; // If active, find results will be highlighted. this.pageContents = []; // Stores the text for each page. this.pageMatches = []; + this.pageMatchesLength = null; this.matchCount = 0; this.selected = { // Currently selected match. pageIdx: -1, @@ -104,10 +105,114 @@ var PDFFindController = (function PDFFindControllerClosure() { }); }, + // Helper for multiple search - fills matchesWithLength array + // and takes into account cases when one search term + // include another search term (for example, "tamed tame" or "this is"). + // Looking for intersecting terms in the 'matches' and + // leave elements with a longer match-length. + + _prepareMatches: function PDFFindController_prepareMatches( + matchesWithLength, matches, matchesLength) { + + function isSubTerm(matchesWithLength, currentIndex) { + var currentElem, prevElem, nextElem; + currentElem = matchesWithLength[currentIndex]; + nextElem = matchesWithLength[currentIndex + 1]; + // checking for cases like "TAMEd TAME" + if (currentIndex < matchesWithLength.length - 1 && + currentElem.match === nextElem.match) { + currentElem.skipped = true; + return true; + } + // checking for cases like "thIS IS" + for (var i = currentIndex - 1; i >= 0; i--) { + 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; + } + + var i, len; + // Sorting array of objects { match: , matchLength: } + // in increasing index first and then the lengths. + matchesWithLength.sort(function(a, b) { + return a.match === b.match ? + a.matchLength - b.matchLength : a.match - b.match; + }); + for (i = 0, len = matchesWithLength.length; i < len; i++) { + if (isSubTerm(matchesWithLength, i)) { + continue; + } + matches.push(matchesWithLength[i].match); + matchesLength.push(matchesWithLength[i].matchLength); + } + }, + + calcFindPhraseMatch: function PDFFindController_calcFindPhraseMatch( + query, pageIndex, pageContent) { + var matches = []; + var queryLen = query.length; + var matchIdx = -queryLen; + while (true) { + matchIdx = pageContent.indexOf(query, matchIdx + queryLen); + if (matchIdx === -1) { + break; + } + matches.push(matchIdx); + } + this.pageMatches[pageIndex] = matches; + }, + + calcFindWordMatch: function PDFFindController_calcFindWordMatch( + query, pageIndex, pageContent) { + var matchesWithLength = []; + // Divide the query into pieces and search for text on each piece. + var queryArray = query.match(/\S+/g); + var subquery, subqueryLen, matchIdx; + for (var i = 0, len = queryArray.length; i < len; i++) { + subquery = queryArray[i]; + subqueryLen = subquery.length; + matchIdx = -subqueryLen; + while (true) { + matchIdx = pageContent.indexOf(subquery, matchIdx + subqueryLen); + if (matchIdx === -1) { + break; + } + // Other searches do not, so we store the length. + matchesWithLength.push({ + match: matchIdx, + matchLength: subqueryLen, + skipped: false + }); + } + } + // Prepare arrays for store the matches. + if (!this.pageMatchesLength) { + this.pageMatchesLength = []; + } + this.pageMatchesLength[pageIndex] = []; + this.pageMatches[pageIndex] = []; + // Sort matchesWithLength, clean up intersecting terms + // and put the result into the two arrays. + this._prepareMatches(matchesWithLength, this.pageMatches[pageIndex], + this.pageMatchesLength[pageIndex]); + }, + calcFindMatch: function PDFFindController_calcFindMatch(pageIndex) { var pageContent = this.normalize(this.pageContents[pageIndex]); var query = this.normalize(this.state.query); var caseSensitive = this.state.caseSensitive; + var phraseSearch = this.state.phraseSearch; var queryLen = query.length; if (queryLen === 0) { @@ -120,16 +225,12 @@ var PDFFindController = (function PDFFindControllerClosure() { query = query.toLowerCase(); } - var matches = []; - var matchIdx = -queryLen; - while (true) { - matchIdx = pageContent.indexOf(query, matchIdx + queryLen); - if (matchIdx === -1) { - break; - } - matches.push(matchIdx); + if (phraseSearch) { + this.calcFindPhraseMatch(query, pageIndex, pageContent); + } else { + this.calcFindWordMatch(query, pageIndex, pageContent); } - this.pageMatches[pageIndex] = matches; + this.updatePage(pageIndex); if (this.resumePageIdx === pageIndex) { this.resumePageIdx = null; @@ -137,8 +238,8 @@ var PDFFindController = (function PDFFindControllerClosure() { } // Update the matches count - if (matches.length > 0) { - this.matchCount += matches.length; + if (this.pageMatches[pageIndex].length > 0) { + this.matchCount += this.pageMatches[pageIndex].length; this.updateUIResultsCount(); } }, @@ -233,6 +334,7 @@ var PDFFindController = (function PDFFindControllerClosure() { this.resumePageIdx = null; this.pageMatches = []; this.matchCount = 0; + this.pageMatchesLength = null; var self = this; for (var i = 0; i < numPages; i++) { diff --git a/web/pdf_link_service.js b/web/pdf_link_service.js index 9228dd914..200389c0f 100644 --- a/web/pdf_link_service.js +++ b/web/pdf_link_service.js @@ -194,6 +194,13 @@ var PDFLinkService = (function () { setHash: function PDFLinkService_setHash(hash) { if (hash.indexOf('=') >= 0) { var params = parseQueryString(hash); + if ('search' in params) { + this.eventBus.dispatch('findfromurlhash', { + source: this, + query: params['search'].replace(/"/g, ''), + phraseSearch: (params['phrase'] === 'true') + }); + } // borrowing syntax from "Parameters for Opening PDF Files" if ('nameddest' in params) { if (this.pdfHistory) { diff --git a/web/text_layer_builder.js b/web/text_layer_builder.js index 98abed00b..eef34cb3f 100644 --- a/web/text_layer_builder.js +++ b/web/text_layer_builder.js @@ -116,7 +116,8 @@ var TextLayerBuilder = (function TextLayerBuilderClosure() { this.divContentDone = true; }, - convertMatches: function TextLayerBuilder_convertMatches(matches) { + convertMatches: function TextLayerBuilder_convertMatches(matches, + matchesLength) { var i = 0; var iIndex = 0; var bidiTexts = this.textContent.items; @@ -124,7 +125,9 @@ var TextLayerBuilder = (function TextLayerBuilderClosure() { var queryLen = (this.findController === null ? 0 : this.findController.state.query.length); var ret = []; - + if (!matches) { + return ret; + } for (var m = 0, len = matches.length; m < len; m++) { // Calculate the start position. var matchIdx = matches[m]; @@ -147,7 +150,11 @@ var TextLayerBuilder = (function TextLayerBuilderClosure() { }; // Calculate the end position. - matchIdx += queryLen; + if (matchesLength) { // multiterm search + matchIdx += matchesLength[m]; + } else { // phrase search + matchIdx += queryLen; + } // Somewhat the same array as above, but use > instead of >= to get // the end position right. @@ -289,8 +296,14 @@ var TextLayerBuilder = (function TextLayerBuilderClosure() { // Convert the matches on the page controller into the match format // used for the textLayer. - this.matches = this.convertMatches(this.findController === null ? - [] : (this.findController.pageMatches[this.pageIdx] || [])); + var pageMatches, pageMatchesLength; + if (this.findController !== null) { + pageMatches = this.findController.pageMatches[this.pageIdx] || null; + pageMatchesLength = (this.findController.pageMatchesLength) ? + this.findController.pageMatchesLength[this.pageIdx] || null : null; + } + + this.matches = this.convertMatches(pageMatches, pageMatchesLength); this.renderMatches(this.matches); },