diff --git a/src/display/display_utils.js b/src/display/display_utils.js index c34035f2c..be4b20c7c 100644 --- a/src/display/display_utils.js +++ b/src/display/display_utils.js @@ -13,24 +13,21 @@ * limitations under the License. */ -import { - assert, - BaseException, - isString, - removeNullCharacters, - shadow, - stringToBytes, - Util, - warn, -} from "../shared/util.js"; import { BaseCanvasFactory, BaseCMapReaderFactory, BaseStandardFontDataFactory, BaseSVGFactory, } from "./base_factory.js"; +import { + BaseException, + isString, + shadow, + stringToBytes, + Util, + warn, +} from "../shared/util.js"; -const DEFAULT_LINK_REL = "noopener noreferrer nofollow"; const SVG_NS = "http://www.w3.org/2000/svg"; const PixelsPerInch = { @@ -316,70 +313,6 @@ class RenderingCancelledException extends BaseException { } } -const LinkTarget = { - NONE: 0, // Default value. - SELF: 1, - BLANK: 2, - PARENT: 3, - TOP: 4, -}; - -/** - * @typedef ExternalLinkParameters - * @typedef {Object} ExternalLinkParameters - * @property {string} url - An absolute URL. - * @property {LinkTarget} [target] - The link target. The default value is - * `LinkTarget.NONE`. - * @property {string} [rel] - The link relationship. The default value is - * `DEFAULT_LINK_REL`. - * @property {boolean} [enabled] - Whether the link should be enabled. The - * default value is true. - */ - -/** - * Adds various attributes (href, title, target, rel) to hyperlinks. - * @param {HTMLAnchorElement} link - The link element. - * @param {ExternalLinkParameters} params - */ -function addLinkAttributes(link, { url, target, rel, enabled = true } = {}) { - assert( - url && typeof url === "string", - 'addLinkAttributes: A valid "url" parameter must provided.' - ); - - const urlNullRemoved = removeNullCharacters(url); - if (enabled) { - link.href = link.title = urlNullRemoved; - } else { - link.href = ""; - link.title = `Disabled: ${urlNullRemoved}`; - link.onclick = () => { - return false; - }; - } - - let targetStr = ""; // LinkTarget.NONE - switch (target) { - case LinkTarget.NONE: - break; - case LinkTarget.SELF: - targetStr = "_self"; - break; - case LinkTarget.BLANK: - targetStr = "_blank"; - break; - case LinkTarget.PARENT: - targetStr = "_parent"; - break; - case LinkTarget.TOP: - targetStr = "_top"; - break; - } - link.target = targetStr; - - link.rel = typeof rel === "string" ? rel : DEFAULT_LINK_REL; -} - function isDataScheme(url) { const ii = url.length; let i = 0; @@ -632,7 +565,6 @@ function getXfaPageViewport(xfaPage, { scale = 1, rotation = 0 }) { } export { - addLinkAttributes, deprecated, DOMCanvasFactory, DOMCMapReaderFactory, @@ -644,7 +576,6 @@ export { isDataScheme, isPdfFile, isValidFetchUrl, - LinkTarget, loadScript, PageViewport, PDFDateString, diff --git a/src/pdf.js b/src/pdf.js index 3a4b38e0f..d89ac3af8 100644 --- a/src/pdf.js +++ b/src/pdf.js @@ -12,7 +12,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* eslint-disable sort-exports/sort-exports */ // eslint-disable-next-line max-len /** @typedef {import("./display/api").PDFDocumentLoadingTask} PDFDocumentLoadingTask */ @@ -20,19 +19,6 @@ /** @typedef {import("./display/api").PDFPageProxy} PDFPageProxy */ /** @typedef {import("./display/api").RenderTask} RenderTask */ -import { - addLinkAttributes, - getFilenameFromUrl, - getPdfFilenameFromUrl, - getXfaPageViewport, - isPdfFile, - isValidFetchUrl, - LinkTarget, - loadScript, - PDFDateString, - PixelsPerInch, - RenderingCancelledException, -} from "./display/display_utils.js"; import { AnnotationMode, CMapCompressionType, @@ -44,7 +30,6 @@ import { OPS, PasswordResponses, PermissionFlag, - removeNullCharacters, shadow, UnexpectedResponseException, UNSUPPORTED_FEATURES, @@ -60,6 +45,17 @@ import { setPDFNetworkStreamFactory, version, } from "./display/api.js"; +import { + getFilenameFromUrl, + getPdfFilenameFromUrl, + getXfaPageViewport, + isPdfFile, + isValidFetchUrl, + loadScript, + PDFDateString, + PixelsPerInch, + RenderingCancelledException, +} from "./display/display_utils.js"; import { AnnotationLayer } from "./display/annotation_layer.js"; import { GlobalWorkerOptions } from "./display/worker_options.js"; import { isNodeJS } from "./shared/is_node.js"; @@ -108,49 +104,38 @@ if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("PRODUCTION")) { } export { - // From "./display/display_utils.js": - addLinkAttributes, - getFilenameFromUrl, - getPdfFilenameFromUrl, - isPdfFile, - LinkTarget, - loadScript, - PDFDateString, - PixelsPerInch, - RenderingCancelledException, - getXfaPageViewport, - // From "./shared/util.js": + AnnotationLayer, AnnotationMode, + build, CMapCompressionType, createObjectURL, createPromiseCapability, createValidAbsoluteUrl, + getDocument, + getFilenameFromUrl, + getPdfFilenameFromUrl, + getXfaPageViewport, + GlobalWorkerOptions, InvalidPDFException, + isPdfFile, + loadScript, + LoopbackPort, MissingPDFException, OPS, PasswordResponses, + PDFDataRangeTransport, + PDFDateString, + PDFWorker, PermissionFlag, - removeNullCharacters, + PixelsPerInch, + RenderingCancelledException, + renderTextLayer, shadow, + SVGGraphics, UnexpectedResponseException, UNSUPPORTED_FEATURES, Util, VerbosityLevel, - // From "./display/api.js": - build, - getDocument, - LoopbackPort, - PDFDataRangeTransport, - PDFWorker, version, - // From "./display/annotation_layer.js": - AnnotationLayer, - // From "./display/worker_options.js": - GlobalWorkerOptions, - // From "./display/text_layer.js": - renderTextLayer, - // From "./display/svg.js": - SVGGraphics, - // From "./display/xfa_layer.js": XfaLayer, }; diff --git a/src/shared/util.js b/src/shared/util.js index 7211254dd..214624236 100644 --- a/src/shared/util.js +++ b/src/shared/util.js @@ -575,23 +575,6 @@ class AbortException extends BaseException { } } -const NullCharactersRegExp = /\x00+/g; -const InvisibleCharactersRegExp = /[\x01-\x1F]/g; - -/** - * @param {string} str - */ -function removeNullCharacters(str, replaceInvisible = false) { - if (typeof str !== "string") { - warn("The argument for removeNullCharacters must be a string."); - return str; - } - if (replaceInvisible) { - str = str.replace(InvisibleCharactersRegExp, " "); - } - return str.replace(NullCharactersRegExp, ""); -} - function bytesToString(bytes) { assert( bytes !== null && typeof bytes === "object" && bytes.length !== undefined, @@ -1185,7 +1168,6 @@ export { PasswordException, PasswordResponses, PermissionFlag, - removeNullCharacters, RenderingIntentFlag, setVerbosityLevel, shadow, diff --git a/test/unit/ui_utils_spec.js b/test/unit/ui_utils_spec.js index d262f1079..c83e2f056 100644 --- a/test/unit/ui_utils_spec.js +++ b/test/unit/ui_utils_spec.js @@ -21,6 +21,7 @@ import { isPortraitOrientation, isValidRotation, parseQueryString, + removeNullCharacters, } from "../../web/ui_utils.js"; describe("ui_utils", function () { @@ -139,6 +140,30 @@ describe("ui_utils", function () { }); }); + describe("removeNullCharacters", function () { + it("should not modify string without null characters", function () { + const str = "string without null chars"; + expect(removeNullCharacters(str)).toEqual("string without null chars"); + }); + + it("should modify string with null characters", function () { + const str = "string\x00With\x00Null\x00Chars"; + expect(removeNullCharacters(str)).toEqual("stringWithNullChars"); + }); + + it("should modify string with non-displayable characters", function () { + const str = Array.from(Array(32).keys()) + .map(x => String.fromCharCode(x) + "a") + .join(""); + // \x00 is replaced by an empty string. + const expected = + "a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a"; + expect(removeNullCharacters(str, /* replaceInvisible */ true)).toEqual( + expected + ); + }); + }); + describe("getPageSizeInches", function () { it("gets page size (in inches)", function () { const page = { diff --git a/test/unit/util_spec.js b/test/unit/util_spec.js index b86d287c4..941e2542c 100644 --- a/test/unit/util_spec.js +++ b/test/unit/util_spec.js @@ -25,7 +25,6 @@ import { isNum, isSameOrigin, isString, - removeNullCharacters, string32, stringToBytes, stringToPDFString, @@ -175,30 +174,6 @@ describe("util", function () { }); }); - describe("removeNullCharacters", function () { - it("should not modify string without null characters", function () { - const str = "string without null chars"; - expect(removeNullCharacters(str)).toEqual("string without null chars"); - }); - - it("should modify string with null characters", function () { - const str = "string\x00With\x00Null\x00Chars"; - expect(removeNullCharacters(str)).toEqual("stringWithNullChars"); - }); - - it("should modify string with non-displayable characters", function () { - const str = Array.from(Array(32).keys()) - .map(x => String.fromCharCode(x) + "a") - .join(""); - // \x00 is replaced by an empty string. - const expected = - "a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a"; - expect(removeNullCharacters(str, /* replaceInvisible */ true)).toEqual( - expected - ); - }); - }); - describe("ReadableStream", function () { it("should return an Object", function () { const readable = new ReadableStream(); diff --git a/web/app.js b/web/app.js index 912880dee..2824358d1 100644 --- a/web/app.js +++ b/web/app.js @@ -46,7 +46,6 @@ import { GlobalWorkerOptions, InvalidPDFException, isPdfFile, - LinkTarget, loadScript, MissingPDFException, OPS, @@ -57,6 +56,7 @@ import { version, } from "pdfjs-lib"; import { CursorTool, PDFCursorTools } from "./pdf_cursor_tools.js"; +import { LinkTarget, PDFLinkService } from "./pdf_link_service.js"; import { OverlayManager } from "./overlay_manager.js"; import { PasswordPrompt } from "./password_prompt.js"; import { PDFAttachmentViewer } from "./pdf_attachment_viewer.js"; @@ -65,7 +65,6 @@ import { PDFFindBar } from "./pdf_find_bar.js"; import { PDFFindController } from "./pdf_find_controller.js"; import { PDFHistory } from "./pdf_history.js"; import { PDFLayerViewer } from "./pdf_layer_viewer.js"; -import { PDFLinkService } from "./pdf_link_service.js"; import { PDFOutlineViewer } from "./pdf_outline_viewer.js"; import { PDFPresentationMode } from "./pdf_presentation_mode.js"; import { PDFRenderingQueue } from "./pdf_rendering_queue.js"; diff --git a/web/base_tree_viewer.js b/web/base_tree_viewer.js index 7a4f1e030..7a3168128 100644 --- a/web/base_tree_viewer.js +++ b/web/base_tree_viewer.js @@ -13,7 +13,7 @@ * limitations under the License. */ -import { removeNullCharacters } from "pdfjs-lib"; +import { removeNullCharacters } from "./ui_utils.js"; const TREEITEM_OFFSET_TOP = -100; // px const TREEITEM_SELECTED_CLASS = "selected"; diff --git a/web/pdf_link_service.js b/web/pdf_link_service.js index 3f6e4eb25..6e2a5f989 100644 --- a/web/pdf_link_service.js +++ b/web/pdf_link_service.js @@ -16,8 +16,72 @@ /** @typedef {import("./event_utils").EventBus} EventBus */ /** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */ -import { addLinkAttributes, LinkTarget } from "pdfjs-lib"; -import { parseQueryString } from "./ui_utils.js"; +import { parseQueryString, removeNullCharacters } from "./ui_utils.js"; + +const DEFAULT_LINK_REL = "noopener noreferrer nofollow"; + +const LinkTarget = { + NONE: 0, // Default value. + SELF: 1, + BLANK: 2, + PARENT: 3, + TOP: 4, +}; + +/** + * @typedef ExternalLinkParameters + * @typedef {Object} ExternalLinkParameters + * @property {string} url - An absolute URL. + * @property {LinkTarget} [target] - The link target. The default value is + * `LinkTarget.NONE`. + * @property {string} [rel] - The link relationship. The default value is + * `DEFAULT_LINK_REL`. + * @property {boolean} [enabled] - Whether the link should be enabled. The + * default value is true. + */ + +/** + * Adds various attributes (href, title, target, rel) to hyperlinks. + * @param {HTMLAnchorElement} link - The link element. + * @param {ExternalLinkParameters} params + */ +function addLinkAttributes(link, { url, target, rel, enabled = true } = {}) { + if (!url || typeof url !== "string") { + throw new Error('A valid "url" parameter must provided.'); + } + + const urlNullRemoved = removeNullCharacters(url); + if (enabled) { + link.href = link.title = urlNullRemoved; + } else { + link.href = ""; + link.title = `Disabled: ${urlNullRemoved}`; + link.onclick = () => { + return false; + }; + } + + let targetStr = ""; // LinkTarget.NONE + switch (target) { + case LinkTarget.NONE: + break; + case LinkTarget.SELF: + targetStr = "_self"; + break; + case LinkTarget.BLANK: + targetStr = "_blank"; + break; + case LinkTarget.PARENT: + targetStr = "_parent"; + break; + case LinkTarget.TOP: + targetStr = "_top"; + break; + } + link.target = targetStr; + + link.rel = typeof rel === "string" ? rel : DEFAULT_LINK_REL; +} /** * @typedef {Object} PDFLinkServiceOptions @@ -38,6 +102,8 @@ import { parseQueryString } from "./ui_utils.js"; * @implements {IPDFLinkService} */ class PDFLinkService { + #pagesRefCache = new Map(); + /** * @param {PDFLinkServiceOptions} options */ @@ -57,14 +123,12 @@ class PDFLinkService { this.pdfDocument = null; this.pdfViewer = null; this.pdfHistory = null; - - this._pagesRefCache = null; } setDocument(pdfDocument, baseUrl = null) { this.baseUrl = baseUrl; this.pdfDocument = pdfDocument; - this._pagesRefCache = Object.create(null); + this.#pagesRefCache.clear(); } setViewer(pdfViewer) { @@ -110,10 +174,7 @@ class PDFLinkService { this.pdfViewer.pagesRotation = value; } - /** - * @private - */ - _goToDestinationHelper(rawDest, namedDest = null, explicitDest) { + #goToDestinationHelper(rawDest, namedDest = null, explicitDest) { // Dest array looks like that: const destRef = explicitDest[0]; let pageNumber; @@ -128,11 +189,11 @@ class PDFLinkService { .getPageIndex(destRef) .then(pageIndex => { this.cachePageRef(pageIndex + 1, destRef); - this._goToDestinationHelper(rawDest, namedDest, explicitDest); + this.#goToDestinationHelper(rawDest, namedDest, explicitDest); }) .catch(() => { console.error( - `PDFLinkService._goToDestinationHelper: "${destRef}" is not ` + + `PDFLinkService.#goToDestinationHelper: "${destRef}" is not ` + `a valid page reference, for dest="${rawDest}".` ); }); @@ -142,14 +203,14 @@ class PDFLinkService { pageNumber = destRef + 1; } else { console.error( - `PDFLinkService._goToDestinationHelper: "${destRef}" is not ` + + `PDFLinkService.#goToDestinationHelper: "${destRef}" is not ` + `a valid destination reference, for dest="${rawDest}".` ); return; } if (!pageNumber || pageNumber < 1 || pageNumber > this.pagesCount) { console.error( - `PDFLinkService._goToDestinationHelper: "${pageNumber}" is not ` + + `PDFLinkService.#goToDestinationHelper: "${pageNumber}" is not ` + `a valid page number, for dest="${rawDest}".` ); return; @@ -193,7 +254,7 @@ class PDFLinkService { ); return; } - this._goToDestinationHelper(dest, namedDest, explicitDest); + this.#goToDestinationHelper(dest, namedDest, explicitDest); } /** @@ -230,7 +291,7 @@ class PDFLinkService { } /** - * Wrapper around the `addLinkAttributes`-function in the API. + * Wrapper around the `addLinkAttributes` helper function. * @param {HTMLAnchorElement} link * @param {string} url * @param {boolean} [newWindow] @@ -340,8 +401,7 @@ class PDFLinkService { } } else { console.error( - `PDFLinkService.setHash: "${zoomArg}" is not ` + - "a valid zoom value." + `PDFLinkService.setHash: "${zoomArg}" is not a valid zoom value.` ); } } @@ -379,13 +439,17 @@ class PDFLinkService { } } catch (ex) {} - if (typeof dest === "string" || isValidExplicitDestination(dest)) { + if ( + typeof dest === "string" || + PDFLinkService.#isValidExplicitDestination(dest) + ) { this.goToDestination(dest); return; } console.error( - `PDFLinkService.setHash: "${unescape(hash)}" is not ` + - "a valid destination." + `PDFLinkService.setHash: "${unescape( + hash + )}" is not a valid destination.` ); } } @@ -440,11 +504,11 @@ class PDFLinkService { } const refStr = pageRef.gen === 0 ? `${pageRef.num}R` : `${pageRef.num}R${pageRef.gen}`; - this._pagesRefCache[refStr] = pageNum; + this.#pagesRefCache.set(refStr, pageNum); } /** - * @private + * @ignore */ _cachedPageNumber(pageRef) { if (!pageRef) { @@ -452,7 +516,7 @@ class PDFLinkService { } const refStr = pageRef.gen === 0 ? `${pageRef.num}R` : `${pageRef.num}R${pageRef.gen}`; - return this._pagesRefCache?.[refStr] || null; + return this.#pagesRefCache.get(refStr) || null; } /** @@ -468,65 +532,65 @@ class PDFLinkService { isPageCached(pageNumber) { return this.pdfViewer.isPageCached(pageNumber); } -} -function isValidExplicitDestination(dest) { - if (!Array.isArray(dest)) { - return false; - } - const destLength = dest.length; - if (destLength < 2) { - return false; - } - const page = dest[0]; - if ( - !( - typeof page === "object" && - Number.isInteger(page.num) && - Number.isInteger(page.gen) - ) && - !(Number.isInteger(page) && page >= 0) - ) { - return false; - } - const zoom = dest[1]; - if (!(typeof zoom === "object" && typeof zoom.name === "string")) { - return false; - } - let allowNull = true; - switch (zoom.name) { - case "XYZ": - if (destLength !== 5) { - return false; - } - break; - case "Fit": - case "FitB": - return destLength === 2; - case "FitH": - case "FitBH": - case "FitV": - case "FitBV": - if (destLength !== 3) { - return false; - } - break; - case "FitR": - if (destLength !== 6) { - return false; - } - allowNull = false; - break; - default: - return false; - } - for (let i = 2; i < destLength; i++) { - const param = dest[i]; - if (!(typeof param === "number" || (allowNull && param === null))) { + static #isValidExplicitDestination(dest) { + if (!Array.isArray(dest)) { return false; } + const destLength = dest.length; + if (destLength < 2) { + return false; + } + const page = dest[0]; + if ( + !( + typeof page === "object" && + Number.isInteger(page.num) && + Number.isInteger(page.gen) + ) && + !(Number.isInteger(page) && page >= 0) + ) { + return false; + } + const zoom = dest[1]; + if (!(typeof zoom === "object" && typeof zoom.name === "string")) { + return false; + } + let allowNull = true; + switch (zoom.name) { + case "XYZ": + if (destLength !== 5) { + return false; + } + break; + case "Fit": + case "FitB": + return destLength === 2; + case "FitH": + case "FitBH": + case "FitV": + case "FitBV": + if (destLength !== 3) { + return false; + } + break; + case "FitR": + if (destLength !== 6) { + return false; + } + allowNull = false; + break; + default: + return false; + } + for (let i = 2; i < destLength; i++) { + const param = dest[i]; + if (!(typeof param === "number" || (allowNull && param === null))) { + return false; + } + } + return true; } - return true; } /** @@ -634,4 +698,4 @@ class SimpleLinkService { } } -export { PDFLinkService, SimpleLinkService }; +export { LinkTarget, PDFLinkService, SimpleLinkService }; diff --git a/web/pdf_viewer.component.js b/web/pdf_viewer.component.js index dce7d9b93..a7a918cf1 100644 --- a/web/pdf_viewer.component.js +++ b/web/pdf_viewer.component.js @@ -19,7 +19,11 @@ import { DefaultTextLayerFactory, DefaultXfaLayerFactory, } from "./default_factory.js"; -import { PDFLinkService, SimpleLinkService } from "./pdf_link_service.js"; +import { + LinkTarget, + PDFLinkService, + SimpleLinkService, +} from "./pdf_link_service.js"; import { PDFSinglePageViewer, PDFViewer } from "./pdf_viewer.js"; import { AnnotationLayerBuilder } from "./annotation_layer_builder.js"; import { DownloadManager } from "./download_manager.js"; @@ -49,6 +53,7 @@ export { DownloadManager, EventBus, GenericL10n, + LinkTarget, NullL10n, PDFFindController, PDFHistory, diff --git a/web/ui_utils.js b/web/ui_utils.js index d446cda3d..54593573a 100644 --- a/web/ui_utils.js +++ b/web/ui_utils.js @@ -200,6 +200,24 @@ function parseQueryString(query) { return params; } +const NullCharactersRegExp = /\x00/g; +const InvisibleCharactersRegExp = /[\x01-\x1F]/g; + +/** + * @param {string} str + * @param {boolean} [replaceInvisible] + */ +function removeNullCharacters(str, replaceInvisible = false) { + if (typeof str !== "string") { + console.error(`The argument must be a string.`); + return str; + } + if (replaceInvisible) { + str = str.replace(InvisibleCharactersRegExp, " "); + } + return str.replace(NullCharactersRegExp, ""); +} + /** * Use binary search to find the index of the first item in a given array which * passes a given condition. The items are expected to be sorted in the sense @@ -838,6 +856,7 @@ export { parseQueryString, PresentationModeState, ProgressBar, + removeNullCharacters, RendererType, RenderingStates, roundToDivide,