Support rich content in markup annotation

- use the xfa parser but in the xhtml namespace.
This commit is contained in:
Calixte Denizet 2021-10-24 17:29:30 +02:00
parent 0e7614df7f
commit cf8dc750d6
14 changed files with 188 additions and 39 deletions

View File

@ -55,6 +55,7 @@ import { ObjectLoader } from "./object_loader.js";
import { OperatorList } from "./operator_list.js"; import { OperatorList } from "./operator_list.js";
import { StringStream } from "./stream.js"; import { StringStream } from "./stream.js";
import { writeDict } from "./writer.js"; import { writeDict } from "./writer.js";
import { XFAFactory } from "./xfa/factory.js";
class AnnotationFactory { class AnnotationFactory {
/** /**
@ -1098,6 +1099,10 @@ class MarkupAnnotation extends Annotation {
this.data.color = null; this.data.color = null;
} }
} }
if (dict.has("RC")) {
this.data.richText = XFAFactory.getRichTextAsHtml(dict.get("RC"));
}
} }
/** /**
@ -2545,6 +2550,10 @@ class PopupAnnotation extends Annotation {
this.setContents(parentItem.get("Contents")); this.setContents(parentItem.get("Contents"));
this.data.contentsObj = this._contents; this.data.contentsObj = this._contents;
if (parentItem.has("RC")) {
this.data.richText = XFAFactory.getRichTextAsHtml(parentItem.get("RC"));
}
} }
} }

View File

@ -66,7 +66,7 @@ class Empty extends XFAObject {
} }
class Builder { class Builder {
constructor() { constructor(rootNameSpace = null) {
this._namespaceStack = []; this._namespaceStack = [];
this._nsAgnosticLevel = 0; this._nsAgnosticLevel = 0;
@ -76,7 +76,8 @@ class Builder {
this._nextNsId = Math.max( this._nextNsId = Math.max(
...Object.values(NamespaceIds).map(({ id }) => id) ...Object.values(NamespaceIds).map(({ id }) => id)
); );
this._currentNamespace = new UnknownNamespace(++this._nextNsId); this._currentNamespace =
rootNameSpace || new UnknownNamespace(++this._nextNsId);
} }
buildRoot(ids) { buildRoot(ids) {

View File

@ -13,13 +13,20 @@
* limitations under the License. * limitations under the License.
*/ */
import { $globalData, $toHTML } from "./xfa_object.js"; import {
$appendChild,
$globalData,
$nodeName,
$text,
$toHTML,
} from "./xfa_object.js";
import { Binder } from "./bind.js"; import { Binder } from "./bind.js";
import { DataHandler } from "./data.js"; import { DataHandler } from "./data.js";
import { FontFinder } from "./fonts.js"; import { FontFinder } from "./fonts.js";
import { stripQuotes } from "./utils.js"; import { stripQuotes } from "./utils.js";
import { warn } from "../../shared/util.js"; import { warn } from "../../shared/util.js";
import { XFAParser } from "./parser.js"; import { XFAParser } from "./parser.js";
import { XhtmlNamespace } from "./xhtml.js";
class XFAFactory { class XFAFactory {
constructor(data) { constructor(data) {
@ -106,6 +113,43 @@ class XFAFactory {
} }
return Object.values(data).join(""); return Object.values(data).join("");
} }
static getRichTextAsHtml(rc) {
if (!rc || typeof rc !== "string") {
return null;
}
try {
let root = new XFAParser(XhtmlNamespace, /* richText */ true).parse(rc);
if (!["body", "xhtml"].includes(root[$nodeName])) {
// No body, so create one.
const newRoot = XhtmlNamespace.body({});
newRoot[$appendChild](root);
root = newRoot;
}
const result = root[$toHTML]();
if (!result.success) {
return null;
}
const { html } = result;
const { attributes } = html;
if (attributes) {
if (attributes.class) {
attributes.class = attributes.class.filter(
attr => !attr.startsWith("xfa")
);
}
attributes.dir = "auto";
}
return { html, str: root[$text]() };
} catch (e) {
warn(`XFA - an error occurred during parsing of rich text: ${e}`);
}
return null;
}
} }
export { XFAFactory }; export { XFAFactory };

View File

@ -606,10 +606,16 @@ function setPara(node, nodeStyle, value) {
} }
function setFontFamily(xfaFont, node, fontFinder, style) { function setFontFamily(xfaFont, node, fontFinder, style) {
const name = stripQuotes(xfaFont.typeface); if (!fontFinder) {
const typeface = fontFinder.find(name); // The font cannot be found in the pdf so use the default one.
delete style.fontFamily;
return;
}
const name = stripQuotes(xfaFont.typeface);
style.fontFamily = `"${name}"`; style.fontFamily = `"${name}"`;
const typeface = fontFinder.find(name);
if (typeface) { if (typeface) {
const { fontFamily } = typeface.regular.cssFontInfo; const { fontFamily } = typeface.regular.cssFontInfo;
if (fontFamily !== name) { if (fontFamily !== name) {

View File

@ -30,9 +30,9 @@ import { Builder } from "./builder.js";
import { warn } from "../../shared/util.js"; import { warn } from "../../shared/util.js";
class XFAParser extends XMLParserBase { class XFAParser extends XMLParserBase {
constructor() { constructor(rootNameSpace = null, richText = false) {
super(); super();
this._builder = new Builder(); this._builder = new Builder(rootNameSpace);
this._stack = []; this._stack = [];
this._globalData = { this._globalData = {
usedTypefaces: new Set(), usedTypefaces: new Set(),
@ -42,6 +42,7 @@ class XFAParser extends XMLParserBase {
this._errorCode = XMLParserErrorCode.NoError; this._errorCode = XMLParserErrorCode.NoError;
this._whiteRegex = /^\s+$/; this._whiteRegex = /^\s+$/;
this._nbsps = /\xa0+/g; this._nbsps = /\xa0+/g;
this._richText = richText;
} }
parse(data) { parse(data) {
@ -60,8 +61,8 @@ class XFAParser extends XMLParserBase {
// Normally by definition a &nbsp is unbreakable // Normally by definition a &nbsp is unbreakable
// but in real life Acrobat can break strings on &nbsp. // but in real life Acrobat can break strings on &nbsp.
text = text.replace(this._nbsps, match => match.slice(1) + " "); text = text.replace(this._nbsps, match => match.slice(1) + " ");
if (this._current[$acceptWhitespace]()) { if (this._richText || this._current[$acceptWhitespace]()) {
this._current[$onText](text); this._current[$onText](text, this._richText);
return; return;
} }

View File

@ -20,6 +20,7 @@ import {
$content, $content,
$extra, $extra,
$getChildren, $getChildren,
$getParent,
$globalData, $globalData,
$nodeName, $nodeName,
$onText, $onText,
@ -38,6 +39,7 @@ import {
import { getMeasurement, HTMLResult, stripQuotes } from "./utils.js"; import { getMeasurement, HTMLResult, stripQuotes } from "./utils.js";
const XHTML_NS_ID = NamespaceIds.xhtml.id; const XHTML_NS_ID = NamespaceIds.xhtml.id;
const $richText = Symbol();
const VALID_STYLES = new Set([ const VALID_STYLES = new Set([
"color", "color",
@ -109,6 +111,7 @@ const StyleMapping = new Map([
const spacesRegExp = /\s+/g; const spacesRegExp = /\s+/g;
const crlfRegExp = /[\r\n]+/g; const crlfRegExp = /[\r\n]+/g;
const crlfForRichTextRegExp = /\r\n?/g;
function mapStyle(styleStr, node) { function mapStyle(styleStr, node) {
const style = Object.create(null); const style = Object.create(null);
@ -185,6 +188,7 @@ const NoWhites = new Set(["body", "html"]);
class XhtmlObject extends XmlObject { class XhtmlObject extends XmlObject {
constructor(attributes, name) { constructor(attributes, name) {
super(XHTML_NS_ID, name); super(XHTML_NS_ID, name);
this[$richText] = false;
this.style = attributes.style || ""; this.style = attributes.style || "";
} }
@ -197,11 +201,16 @@ class XhtmlObject extends XmlObject {
return !NoWhites.has(this[$nodeName]); return !NoWhites.has(this[$nodeName]);
} }
[$onText](str) { [$onText](str, richText = false) {
str = str.replace(crlfRegExp, ""); if (!richText) {
if (!this.style.includes("xfa-spacerun:yes")) { str = str.replace(crlfRegExp, "");
str = str.replace(spacesRegExp, " "); if (!this.style.includes("xfa-spacerun:yes")) {
str = str.replace(spacesRegExp, " ");
}
} else {
this[$richText] = true;
} }
if (str) { if (str) {
this[$content] += str; this[$content] += str;
} }
@ -311,6 +320,15 @@ class XhtmlObject extends XmlObject {
return HTMLResult.EMPTY; return HTMLResult.EMPTY;
} }
let value;
if (this[$richText]) {
value = this[$content]
? this[$content].replace(crlfForRichTextRegExp, "\n")
: undefined;
} else {
value = this[$content] || undefined;
}
return HTMLResult.success({ return HTMLResult.success({
name: this[$nodeName], name: this[$nodeName],
attributes: { attributes: {
@ -318,7 +336,7 @@ class XhtmlObject extends XmlObject {
style: mapStyle(this.style, this), style: mapStyle(this.style, this),
}, },
children, children,
value: this[$content] || "", value,
}); });
} }
} }
@ -457,6 +475,10 @@ class P extends XhtmlObject {
} }
[$text]() { [$text]() {
const siblings = this[$getParent]()[$getChildren]();
if (siblings[siblings.length - 1] === this) {
return super[$text]();
}
return super[$text]() + "\n"; return super[$text]() + "\n";
} }
} }

View File

@ -30,6 +30,7 @@ import {
} from "./display_utils.js"; } from "./display_utils.js";
import { AnnotationStorage } from "./annotation_storage.js"; import { AnnotationStorage } from "./annotation_storage.js";
import { ColorConverters } from "../shared/scripting_utils.js"; import { ColorConverters } from "../shared/scripting_utils.js";
import { XfaLayer } from "./xfa_layer.js";
const DEFAULT_TAB_INDEX = 1000; const DEFAULT_TAB_INDEX = 1000;
const GetElementsByNameSet = new WeakSet(); const GetElementsByNameSet = new WeakSet();
@ -322,6 +323,7 @@ class AnnotationElement {
titleObj: data.titleObj, titleObj: data.titleObj,
modificationDate: data.modificationDate, modificationDate: data.modificationDate,
contentsObj: data.contentsObj, contentsObj: data.contentsObj,
richText: data.richText,
hideWrapper: true, hideWrapper: true,
}); });
const popup = popupElement.render(); const popup = popupElement.render();
@ -676,7 +678,8 @@ class TextAnnotationElement extends AnnotationElement {
const isRenderable = !!( const isRenderable = !!(
parameters.data.hasPopup || parameters.data.hasPopup ||
parameters.data.titleObj?.str || parameters.data.titleObj?.str ||
parameters.data.contentsObj?.str parameters.data.contentsObj?.str ||
parameters.data.richText?.str
); );
super(parameters, { isRenderable }); super(parameters, { isRenderable });
} }
@ -1546,7 +1549,9 @@ class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement {
class PopupAnnotationElement extends AnnotationElement { class PopupAnnotationElement extends AnnotationElement {
constructor(parameters) { constructor(parameters) {
const isRenderable = !!( const isRenderable = !!(
parameters.data.titleObj?.str || parameters.data.contentsObj?.str parameters.data.titleObj?.str ||
parameters.data.contentsObj?.str ||
parameters.data.richText?.str
); );
super(parameters, { isRenderable }); super(parameters, { isRenderable });
} }
@ -1582,6 +1587,7 @@ class PopupAnnotationElement extends AnnotationElement {
titleObj: this.data.titleObj, titleObj: this.data.titleObj,
modificationDate: this.data.modificationDate, modificationDate: this.data.modificationDate,
contentsObj: this.data.contentsObj, contentsObj: this.data.contentsObj,
richText: this.data.richText,
}); });
// Position the popup next to the parent annotation's container. // Position the popup next to the parent annotation's container.
@ -1614,6 +1620,7 @@ class PopupElement {
this.titleObj = parameters.titleObj; this.titleObj = parameters.titleObj;
this.modificationDate = parameters.modificationDate; this.modificationDate = parameters.modificationDate;
this.contentsObj = parameters.contentsObj; this.contentsObj = parameters.contentsObj;
this.richText = parameters.richText;
this.hideWrapper = parameters.hideWrapper || false; this.hideWrapper = parameters.hideWrapper || false;
this.pinned = false; this.pinned = false;
@ -1655,6 +1662,7 @@ class PopupElement {
const dateObject = PDFDateString.toDateObject(this.modificationDate); const dateObject = PDFDateString.toDateObject(this.modificationDate);
if (dateObject) { if (dateObject) {
const modificationDate = document.createElement("span"); const modificationDate = document.createElement("span");
modificationDate.className = "popupDate";
modificationDate.textContent = "{{date}}, {{time}}"; modificationDate.textContent = "{{date}}, {{time}}";
modificationDate.dataset.l10nId = "annotation_date_string"; modificationDate.dataset.l10nId = "annotation_date_string";
modificationDate.dataset.l10nArgs = JSON.stringify({ modificationDate.dataset.l10nArgs = JSON.stringify({
@ -1664,8 +1672,20 @@ class PopupElement {
popup.appendChild(modificationDate); popup.appendChild(modificationDate);
} }
const contents = this._formatContents(this.contentsObj); if (
popup.appendChild(contents); this.richText?.str &&
(!this.contentsObj?.str || this.contentsObj.str === this.richText.str)
) {
XfaLayer.render({
xfa: this.richText.html,
intent: "richText",
div: popup,
});
popup.lastChild.className = "richText popupContent";
} else {
const contents = this._formatContents(this.contentsObj);
popup.appendChild(contents);
}
if (!Array.isArray(this.trigger)) { if (!Array.isArray(this.trigger)) {
this.trigger = [this.trigger]; this.trigger = [this.trigger];
@ -1693,6 +1713,7 @@ class PopupElement {
*/ */
_formatContents({ str, dir }) { _formatContents({ str, dir }) {
const p = document.createElement("p"); const p = document.createElement("p");
p.className = "popupContent";
p.dir = dir; p.dir = dir;
const lines = str.split(/(?:\r\n?|\n)/); const lines = str.split(/(?:\r\n?|\n)/);
for (let i = 0, ii = lines.length; i < ii; ++i) { for (let i = 0, ii = lines.length; i < ii; ++i) {
@ -1759,7 +1780,8 @@ class FreeTextAnnotationElement extends AnnotationElement {
const isRenderable = !!( const isRenderable = !!(
parameters.data.hasPopup || parameters.data.hasPopup ||
parameters.data.titleObj?.str || parameters.data.titleObj?.str ||
parameters.data.contentsObj?.str parameters.data.contentsObj?.str ||
parameters.data.richText?.str
); );
super(parameters, { isRenderable, ignoreBorder: true }); super(parameters, { isRenderable, ignoreBorder: true });
} }
@ -1779,7 +1801,8 @@ class LineAnnotationElement extends AnnotationElement {
const isRenderable = !!( const isRenderable = !!(
parameters.data.hasPopup || parameters.data.hasPopup ||
parameters.data.titleObj?.str || parameters.data.titleObj?.str ||
parameters.data.contentsObj?.str parameters.data.contentsObj?.str ||
parameters.data.richText?.str
); );
super(parameters, { isRenderable, ignoreBorder: true }); super(parameters, { isRenderable, ignoreBorder: true });
} }
@ -1824,7 +1847,8 @@ class SquareAnnotationElement extends AnnotationElement {
const isRenderable = !!( const isRenderable = !!(
parameters.data.hasPopup || parameters.data.hasPopup ||
parameters.data.titleObj?.str || parameters.data.titleObj?.str ||
parameters.data.contentsObj?.str parameters.data.contentsObj?.str ||
parameters.data.richText?.str
); );
super(parameters, { isRenderable, ignoreBorder: true }); super(parameters, { isRenderable, ignoreBorder: true });
} }
@ -1871,7 +1895,8 @@ class CircleAnnotationElement extends AnnotationElement {
const isRenderable = !!( const isRenderable = !!(
parameters.data.hasPopup || parameters.data.hasPopup ||
parameters.data.titleObj?.str || parameters.data.titleObj?.str ||
parameters.data.contentsObj?.str parameters.data.contentsObj?.str ||
parameters.data.richText?.str
); );
super(parameters, { isRenderable, ignoreBorder: true }); super(parameters, { isRenderable, ignoreBorder: true });
} }
@ -1918,7 +1943,8 @@ class PolylineAnnotationElement extends AnnotationElement {
const isRenderable = !!( const isRenderable = !!(
parameters.data.hasPopup || parameters.data.hasPopup ||
parameters.data.titleObj?.str || parameters.data.titleObj?.str ||
parameters.data.contentsObj?.str parameters.data.contentsObj?.str ||
parameters.data.richText?.str
); );
super(parameters, { isRenderable, ignoreBorder: true }); super(parameters, { isRenderable, ignoreBorder: true });
@ -1983,7 +2009,8 @@ class CaretAnnotationElement extends AnnotationElement {
const isRenderable = !!( const isRenderable = !!(
parameters.data.hasPopup || parameters.data.hasPopup ||
parameters.data.titleObj?.str || parameters.data.titleObj?.str ||
parameters.data.contentsObj?.str parameters.data.contentsObj?.str ||
parameters.data.richText?.str
); );
super(parameters, { isRenderable, ignoreBorder: true }); super(parameters, { isRenderable, ignoreBorder: true });
} }
@ -2003,7 +2030,8 @@ class InkAnnotationElement extends AnnotationElement {
const isRenderable = !!( const isRenderable = !!(
parameters.data.hasPopup || parameters.data.hasPopup ||
parameters.data.titleObj?.str || parameters.data.titleObj?.str ||
parameters.data.contentsObj?.str parameters.data.contentsObj?.str ||
parameters.data.richText?.str
); );
super(parameters, { isRenderable, ignoreBorder: true }); super(parameters, { isRenderable, ignoreBorder: true });
@ -2062,7 +2090,8 @@ class HighlightAnnotationElement extends AnnotationElement {
const isRenderable = !!( const isRenderable = !!(
parameters.data.hasPopup || parameters.data.hasPopup ||
parameters.data.titleObj?.str || parameters.data.titleObj?.str ||
parameters.data.contentsObj?.str parameters.data.contentsObj?.str ||
parameters.data.richText?.str
); );
super(parameters, { super(parameters, {
isRenderable, isRenderable,
@ -2090,7 +2119,8 @@ class UnderlineAnnotationElement extends AnnotationElement {
const isRenderable = !!( const isRenderable = !!(
parameters.data.hasPopup || parameters.data.hasPopup ||
parameters.data.titleObj?.str || parameters.data.titleObj?.str ||
parameters.data.contentsObj?.str parameters.data.contentsObj?.str ||
parameters.data.richText?.str
); );
super(parameters, { super(parameters, {
isRenderable, isRenderable,
@ -2118,7 +2148,8 @@ class SquigglyAnnotationElement extends AnnotationElement {
const isRenderable = !!( const isRenderable = !!(
parameters.data.hasPopup || parameters.data.hasPopup ||
parameters.data.titleObj?.str || parameters.data.titleObj?.str ||
parameters.data.contentsObj?.str parameters.data.contentsObj?.str ||
parameters.data.richText?.str
); );
super(parameters, { super(parameters, {
isRenderable, isRenderable,
@ -2146,7 +2177,8 @@ class StrikeOutAnnotationElement extends AnnotationElement {
const isRenderable = !!( const isRenderable = !!(
parameters.data.hasPopup || parameters.data.hasPopup ||
parameters.data.titleObj?.str || parameters.data.titleObj?.str ||
parameters.data.contentsObj?.str parameters.data.contentsObj?.str ||
parameters.data.richText?.str
); );
super(parameters, { super(parameters, {
isRenderable, isRenderable,
@ -2174,7 +2206,8 @@ class StampAnnotationElement extends AnnotationElement {
const isRenderable = !!( const isRenderable = !!(
parameters.data.hasPopup || parameters.data.hasPopup ||
parameters.data.titleObj?.str || parameters.data.titleObj?.str ||
parameters.data.contentsObj?.str parameters.data.contentsObj?.str ||
parameters.data.richText?.str
); );
super(parameters, { isRenderable, ignoreBorder: true }); super(parameters, { isRenderable, ignoreBorder: true });
} }
@ -2215,7 +2248,9 @@ class FileAttachmentAnnotationElement extends AnnotationElement {
if ( if (
!this.data.hasPopup && !this.data.hasPopup &&
(this.data.titleObj?.str || this.data.contentsObj?.str) (this.data.titleObj?.str ||
this.data.contentsObj?.str ||
this.data.richText)
) { ) {
this._createPopup(trigger, this.data); this._createPopup(trigger, this.data);
} }

View File

@ -106,7 +106,9 @@ class XfaLayer {
if (key === "textContent") { if (key === "textContent") {
html.textContent = value; html.textContent = value;
} else if (key === "class") { } else if (key === "class") {
html.setAttribute(key, value.join(" ")); if (value.length) {
html.setAttribute(key, value.join(" "));
}
} else { } else {
if (isHTMLAnchorElement && (key === "href" || key === "newWindow")) { if (isHTMLAnchorElement && (key === "href" || key === "newWindow")) {
continue; // Handled below. continue; // Handled below.
@ -159,11 +161,16 @@ class XfaLayer {
const rootDiv = parameters.div; const rootDiv = parameters.div;
rootDiv.appendChild(rootHtml); rootDiv.appendChild(rootHtml);
const transform = `matrix(${parameters.viewport.transform.join(",")})`;
rootDiv.style.transform = transform; if (parameters.viewport) {
const transform = `matrix(${parameters.viewport.transform.join(",")})`;
rootDiv.style.transform = transform;
}
// Set defaults. // Set defaults.
rootDiv.setAttribute("class", "xfaLayer xfaFont"); if (intent !== "richText") {
rootDiv.setAttribute("class", "xfaLayer xfaFont");
}
// Text nodes used for the text highlighter. // Text nodes used for the text highlighter.
const textDivs = []; const textDivs = [];

View File

@ -479,3 +479,4 @@
!pr12564.pdf !pr12564.pdf
!pr12828.pdf !pr12828.pdf
!secHandler.pdf !secHandler.pdf
!rc_annotation.pdf

View File

@ -0,0 +1 @@
https://github.com/mozilla/pdf.js/files/7024938/2102.12353.pdf

BIN
test/pdfs/rc_annotation.pdf Normal file

Binary file not shown.

View File

@ -137,6 +137,24 @@
"type": "eq", "type": "eq",
"annotations": true "annotations": true
}, },
{ "id": "issue2966",
"file": "pdfs/rc_annotation.pdf",
"md5": "7b978a8c2871b8902656adb67f7bd117",
"rounds": 1,
"lastPage": 1,
"type": "eq",
"annotations": true
},
{ "id": "issue13915",
"file": "pdfs/issue13915.pdf",
"md5": "fef3108733bbf80ea8551feedb427b1c",
"rounds": 1,
"firstPage": 51,
"lastPage": 51,
"link": true,
"type": "eq",
"annotations": true
},
{ "id": "bug946506", { "id": "bug946506",
"file": "pdfs/bug946506.pdf", "file": "pdfs/bug946506.pdf",
"md5": "c28911b5c31bdc337c2ce404c5971cfc", "md5": "c28911b5c31bdc337c2ce404c5971cfc",

View File

@ -46,7 +46,7 @@ describe("XFAParser", function () {
forbidden forbidden
</dynamicRender> </dynamicRender>
</acrobat7> </acrobat7>
<autoSave>enabled</autoSave> <autoSave>enabled</autoSave>
<submitUrl> <submitUrl>
http://d.e.f http://d.e.f
</submitUrl> </submitUrl>
@ -414,7 +414,7 @@ describe("XFAParser", function () {
[ [
" The first line of this paragraph is indented a half-inch.\n", " The first line of this paragraph is indented a half-inch.\n",
" Successive lines are not indented.\n", " Successive lines are not indented.\n",
" This is the last line of the paragraph.\n \n", " This is the last line of the paragraph.\n ",
].join("") ].join("")
); );
}); });

View File

@ -192,17 +192,21 @@
display: inline-block; display: inline-block;
} }
.annotationLayer .popup span { .annotationLayer .popupDate {
display: inline-block; display: inline-block;
margin-left: 5px; margin-left: 5px;
} }
.annotationLayer .popup p { .annotationLayer .popupContent {
border-top: 1px solid rgba(51, 51, 51, 1); border-top: 1px solid rgba(51, 51, 51, 1);
margin-top: 2px; margin-top: 2px;
padding-top: 2px; padding-top: 2px;
} }
.annotationLayer .richText > * {
white-space: pre-wrap;
}
.annotationLayer .highlightAnnotation, .annotationLayer .highlightAnnotation,
.annotationLayer .underlineAnnotation, .annotationLayer .underlineAnnotation,
.annotationLayer .squigglyAnnotation, .annotationLayer .squigglyAnnotation,