diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index 958a5cda33f9f..671ac870ef157 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -49,14 +49,11 @@ import { Util, warn, } from "../shared/util.js"; -import { - PDFDateString, - renderRichText, - setLayerDimensions, -} from "./display_utils.js"; +import { PDFDateString, setLayerDimensions } from "./display_utils.js"; import { AnnotationStorage } from "./annotation_storage.js"; import { ColorConverters } from "../shared/scripting_utils.js"; import { DOMSVGFactory } from "./svg_factory.js"; +import { renderRichText } from "./annotation_layer_utils.js"; const DEFAULT_FONT_SIZE = 9; const GetElementsByNameSet = new WeakSet(); diff --git a/src/display/annotation_layer_utils.js b/src/display/annotation_layer_utils.js new file mode 100644 index 0000000000000..c2cc4665091cb --- /dev/null +++ b/src/display/annotation_layer_utils.js @@ -0,0 +1,43 @@ +/* Copyright 2015 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. + */ + +import { XfaLayer } from "./xfa_layer.js"; + +function renderRichText({ html, dir, className }, container) { + const fragment = document.createDocumentFragment(); + if (typeof html === "string") { + const p = document.createElement("p"); + p.dir = dir || "auto"; + const lines = html.split(/\r\n?|\n/); + for (let i = 0, ii = lines.length; i < ii; ++i) { + const line = lines[i]; + p.append(document.createTextNode(line)); + if (i < ii - 1) { + p.append(document.createElement("br")); + } + } + fragment.append(p); + } else { + XfaLayer.render({ + xfaHtml: html, + div: fragment, + intent: "richText", + }); + } + fragment.firstElementChild.classList.add("richText", className); + container.append(fragment); +} + +export { renderRichText }; diff --git a/src/display/display_utils.js b/src/display/display_utils.js index 989568379cfd3..33baf54f3c687 100644 --- a/src/display/display_utils.js +++ b/src/display/display_utils.js @@ -23,7 +23,6 @@ import { } from "../shared/util.js"; import { MathClamp } from "../shared/math_clamp.js"; import { PageViewport } from "./page_viewport.js"; -import { XfaLayer } from "./xfa_layer.js"; class PixelsPerInch { static CSS = 96.0; @@ -762,31 +761,6 @@ function findContrastColor(baseColor, fixedColor) { return cachedValue; } -function renderRichText({ html, dir, className }, container) { - const fragment = document.createDocumentFragment(); - if (typeof html === "string") { - const p = document.createElement("p"); - p.dir = dir || "auto"; - const lines = html.split(/\r\n?|\n/); - for (let i = 0, ii = lines.length; i < ii; ++i) { - const line = lines[i]; - p.append(document.createTextNode(line)); - if (i < ii - 1) { - p.append(document.createElement("br")); - } - } - fragment.append(p); - } else { - XfaLayer.render({ - xfaHtml: html, - div: fragment, - intent: "richText", - }); - } - fragment.firstElementChild.classList.add("richText", className); - container.append(fragment); -} - function makePathFromDrawOPS(data) { // Using a SVG string is slightly slower than using the following loop. const path = new Path2D(); @@ -849,7 +823,6 @@ export { PDFDateString, PixelsPerInch, RenderingCancelledException, - renderRichText, setLayerDimensions, StatTimer, stopEvent, diff --git a/src/pdf.js b/src/pdf.js index 6be3e8a62e761..9a51dd210d4e9 100644 --- a/src/pdf.js +++ b/src/pdf.js @@ -64,7 +64,6 @@ import { PDFDateString, PixelsPerInch, RenderingCancelledException, - renderRichText, setLayerDimensions, stopEvent, SupportedImageMimeTypes, @@ -86,6 +85,7 @@ import { GlobalWorkerOptions } from "./display/worker_options.js"; import { HighlightOutliner } from "./display/editor/drawers/highlight.js"; import { isValidExplicitDest } from "./display/api_utils.js"; import { MathClamp } from "./shared/math_clamp.js"; +import { renderRichText } from "./display/annotation_layer_utils.js"; import { SignatureExtractor } from "./display/editor/drawers/signaturedraw.js"; import { TextLayer } from "./display/text_layer.js"; import { TextLayerImages } from "./display/text_layer_images.js"; diff --git a/test/unit/annotation_layer_utils_spec.js b/test/unit/annotation_layer_utils_spec.js new file mode 100644 index 0000000000000..f827027c332c2 --- /dev/null +++ b/test/unit/annotation_layer_utils_spec.js @@ -0,0 +1,159 @@ +/* Copyright 2017 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. + */ + +import { isNodeJS } from "../../src/shared/util.js"; +import { renderRichText } from "../../src/display/annotation_layer_utils.js"; + +describe("annotation_layer_utils", function () { + describe("renderRichText", function () { + // Unlike other tests we cannot simply compare the HTML-strings since + // Chrome and Firefox produce different results. Instead we compare sets + // containing the individual parts of the HTML-strings. + const splitParts = s => new Set(s.split(/[<>/ ]+/).filter(Boolean)); + + it("should render plain text", function () { + if (isNodeJS) { + pending("DOM is not supported in Node.js."); + } + const container = document.createElement("div"); + renderRichText( + { + html: "Hello world!\nThis is a test.", + dir: "ltr", + className: "foo", + }, + container + ); + expect(splitParts(container.innerHTML)).toEqual( + splitParts( + '

Hello world!
This is a test.

' + ) + ); + }); + + it("should render XFA rich text", function () { + if (isNodeJS) { + pending("DOM is not supported in Node.js."); + } + const container = document.createElement("div"); + const xfaHtml = { + name: "div", + attributes: { style: { color: "red" } }, + children: [ + { + name: "p", + attributes: { style: { fontSize: "20px" } }, + children: [ + { + name: "span", + attributes: { style: { fontWeight: "bold" } }, + value: "Hello", + }, + { name: "#text", value: " world!" }, + ], + }, + ], + }; + renderRichText( + { html: xfaHtml, dir: "ltr", className: "foo" }, + container + ); + expect(splitParts(container.innerHTML)).toEqual( + splitParts( + '
' + + '

' + + 'Hello world!

' + ) + ); + }); + + it("should only keep the supported rich text elements", function () { + if (isNodeJS) { + pending("DOM is not supported in Node.js."); + } + const container = document.createElement("div"); + const xfaHtml = { + name: "div", + children: [ + { name: "p", value: "kept" }, + { + name: "section", + children: [{ name: "span", value: "removed" }], + }, + ], + }; + renderRichText( + { html: xfaHtml, dir: "ltr", className: "foo" }, + container + ); + + expect(container.querySelector("p")).not.toBeNull(); + expect(container.querySelector("section")).toBeNull(); + expect(container.querySelector("span")).toBeNull(); + expect(container.textContent).toEqual("kept"); + }); + + it("should only keep the supported rich text attributes", function () { + if (isNodeJS) { + pending("DOM is not supported in Node.js."); + } + const container = document.createElement("div"); + const xfaHtml = { + name: "div", + children: [ + { + name: "p", + attributes: { class: ["bar"], dir: "rtl", title: "unsupported" }, + value: "text", + }, + ], + }; + renderRichText( + { html: xfaHtml, dir: "ltr", className: "foo" }, + container + ); + const p = container.querySelector("p"); + + expect(p.getAttribute("class")).toEqual("bar"); + expect(p.getAttribute("dir")).toEqual("rtl"); + expect(p.hasAttribute("title")).toEqual(false); + }); + + it("should only apply the supported rich text style properties", function () { + if (isNodeJS) { + pending("DOM is not supported in Node.js."); + } + const container = document.createElement("div"); + const xfaHtml = { + name: "div", + children: [ + { + name: "span", + attributes: { style: { color: "green", width: "100px" } }, + value: "text", + }, + ], + }; + renderRichText( + { html: xfaHtml, dir: "ltr", className: "foo" }, + container + ); + const span = container.querySelector("span"); + + expect(span.style.color).toEqual("green"); + expect(span.style.width).toEqual(""); + }); + }); +}); diff --git a/test/unit/clitests.json b/test/unit/clitests.json index c98345d3c34b6..3872ccf318cfa 100644 --- a/test/unit/clitests.json +++ b/test/unit/clitests.json @@ -4,6 +4,7 @@ "helpers": ["clitests_helper.js"], "spec_files": [ + "annotation_layer_utils_spec.js", "annotation_spec.js", "annotation_storage_spec.js", "api_spec.js", diff --git a/test/unit/display_utils_spec.js b/test/unit/display_utils_spec.js index e5c2344a1fc15..f7824dcb64d9a 100644 --- a/test/unit/display_utils_spec.js +++ b/test/unit/display_utils_spec.js @@ -22,7 +22,6 @@ import { getRGBA, isValidFetchUrl, PDFDateString, - renderRichText, } from "../../src/display/display_utils.js"; import { isNodeJS } from "../../src/shared/util.js"; @@ -402,144 +401,4 @@ describe("display_utils", function () { expect(applyOpacity([123, 45, 67], ctx.globalAlpha)).toEqual([r, g, b]); }); }); - - describe("renderRichText", function () { - // Unlike other tests we cannot simply compare the HTML-strings since - // Chrome and Firefox produce different results. Instead we compare sets - // containing the individual parts of the HTML-strings. - const splitParts = s => new Set(s.split(/[<>/ ]+/).filter(Boolean)); - - it("should render plain text", function () { - if (isNodeJS) { - pending("DOM is not supported in Node.js."); - } - const container = document.createElement("div"); - renderRichText( - { - html: "Hello world!\nThis is a test.", - dir: "ltr", - className: "foo", - }, - container - ); - expect(splitParts(container.innerHTML)).toEqual( - splitParts( - '

Hello world!
This is a test.

' - ) - ); - }); - - it("should render XFA rich text", function () { - if (isNodeJS) { - pending("DOM is not supported in Node.js."); - } - const container = document.createElement("div"); - const xfaHtml = { - name: "div", - attributes: { style: { color: "red" } }, - children: [ - { - name: "p", - attributes: { style: { fontSize: "20px" } }, - children: [ - { - name: "span", - attributes: { style: { fontWeight: "bold" } }, - value: "Hello", - }, - { name: "#text", value: " world!" }, - ], - }, - ], - }; - renderRichText( - { html: xfaHtml, dir: "ltr", className: "foo" }, - container - ); - expect(splitParts(container.innerHTML)).toEqual( - splitParts( - '
' + - '

' + - 'Hello world!

' - ) - ); - }); - - it("should only keep the supported rich text elements", function () { - if (isNodeJS) { - pending("DOM is not supported in Node.js."); - } - const container = document.createElement("div"); - const xfaHtml = { - name: "div", - children: [ - { name: "p", value: "kept" }, - { - name: "section", - children: [{ name: "span", value: "removed" }], - }, - ], - }; - renderRichText( - { html: xfaHtml, dir: "ltr", className: "foo" }, - container - ); - - expect(container.querySelector("p")).not.toBeNull(); - expect(container.querySelector("section")).toBeNull(); - expect(container.querySelector("span")).toBeNull(); - expect(container.textContent).toEqual("kept"); - }); - - it("should only keep the supported rich text attributes", function () { - if (isNodeJS) { - pending("DOM is not supported in Node.js."); - } - const container = document.createElement("div"); - const xfaHtml = { - name: "div", - children: [ - { - name: "p", - attributes: { class: ["bar"], dir: "rtl", title: "unsupported" }, - value: "text", - }, - ], - }; - renderRichText( - { html: xfaHtml, dir: "ltr", className: "foo" }, - container - ); - const p = container.querySelector("p"); - - expect(p.getAttribute("class")).toEqual("bar"); - expect(p.getAttribute("dir")).toEqual("rtl"); - expect(p.hasAttribute("title")).toEqual(false); - }); - - it("should only apply the supported rich text style properties", function () { - if (isNodeJS) { - pending("DOM is not supported in Node.js."); - } - const container = document.createElement("div"); - const xfaHtml = { - name: "div", - children: [ - { - name: "span", - attributes: { style: { color: "green", width: "100px" } }, - value: "text", - }, - ], - }; - renderRichText( - { html: xfaHtml, dir: "ltr", className: "foo" }, - container - ); - const span = container.querySelector("span"); - - expect(span.style.color).toEqual("green"); - expect(span.style.width).toEqual(""); - }); - }); }); diff --git a/test/unit/jasmine-boot.js b/test/unit/jasmine-boot.js index e6605e65ad1aa..9d61827e31053 100644 --- a/test/unit/jasmine-boot.js +++ b/test/unit/jasmine-boot.js @@ -49,6 +49,7 @@ import { TestReporter } from "../reporter.js"; async function initializePDFJS(callback) { await Promise.all( [ + "pdfjs-test/unit/annotation_layer_utils_spec.js", "pdfjs-test/unit/annotation_spec.js", "pdfjs-test/unit/annotation_storage_spec.js", "pdfjs-test/unit/api_spec.js", diff --git a/test/unit/pdf_spec.js b/test/unit/pdf_spec.js index 57295541c9b79..80d5d45c3664e 100644 --- a/test/unit/pdf_spec.js +++ b/test/unit/pdf_spec.js @@ -55,7 +55,6 @@ import { PDFDateString, PixelsPerInch, RenderingCancelledException, - renderRichText, setLayerDimensions, stopEvent, SupportedImageMimeTypes, @@ -76,6 +75,7 @@ import { DrawLayer } from "../../src/display/draw_layer.js"; import { GlobalWorkerOptions } from "../../src/display/worker_options.js"; import { isValidExplicitDest } from "../../src/display/api_utils.js"; import { MathClamp } from "../../src/shared/math_clamp.js"; +import { renderRichText } from "../../src/display/annotation_layer_utils.js"; import { SignatureExtractor } from "../../src/display/editor/drawers/signaturedraw.js"; import { TextLayer } from "../../src/display/text_layer.js"; import { TextLayerImages } from "../../src/display/text_layer_images.js";