zotero-better-notes/src/utils/annotation.ts

248 lines
7.7 KiB
TypeScript

import { importImageToNote } from "./note";
declare type CustomAnnotationJSON =
Partial<_ZoteroTypes.Annotations.AnnotationJson> & {
id?: string;
attachmentItemID?: number;
text?: string;
tags: any;
imageAttachmentKey?: string | undefined;
};
async function parseAnnotationJSON(annotationItem: Zotero.Item) {
try {
if (!annotationItem || !annotationItem.isAnnotation()) {
return null;
}
const annotationJSON = await Zotero.Annotations.toJSON(annotationItem);
const annotationObj = Object.assign(
{},
annotationJSON
) as CustomAnnotationJSON;
annotationObj.id = annotationItem.key;
annotationObj.attachmentItemID = annotationItem.parentItem?.id;
delete annotationObj.key;
for (const key in annotationObj) {
annotationObj[key as keyof typeof annotationObj] =
annotationObj[key as keyof typeof annotationObj] || ("" as any);
}
annotationObj.tags = annotationObj.tags || [];
return annotationObj as Required<CustomAnnotationJSON>;
} catch (e: unknown) {
Zotero.logError(e as Error);
return null;
}
}
// Zotero.EditorInstanceUtilities.serializeAnnotations
function serializeAnnotations(
annotations: Required<CustomAnnotationJSON>[],
skipEmbeddingItemData: boolean = false,
skipCitation: boolean = false
) {
let storedCitationItems = [];
let html = "";
for (let annotation of annotations) {
let attachmentItem = Zotero.Items.get(annotation.attachmentItemID);
if (!attachmentItem) {
continue;
}
if (
(!annotation.text &&
!annotation.comment &&
!annotation.imageAttachmentKey &&
!annotation.image) ||
annotation.type === "ink"
) {
continue;
}
let citationHTML = "";
let imageHTML = "";
let highlightHTML = "";
let quotedHighlightHTML = "";
let commentHTML = "";
let storedAnnotation: any = {
attachmentURI: Zotero.URI.getItemURI(attachmentItem),
annotationKey: annotation.id,
color: annotation.color,
pageLabel: annotation.pageLabel,
position: annotation.position,
};
// Citation
let parentItem = skipCitation
? undefined
: attachmentItem.parentID && Zotero.Items.get(attachmentItem.parentID);
if (parentItem) {
let uris = [Zotero.URI.getItemURI(parentItem)];
let citationItem: any = {
uris,
locator: annotation.pageLabel,
};
// Note: integration.js` uses `Zotero.Cite.System.prototype.retrieveItem`,
// which produces a little bit different CSL JSON
// @ts-ignore
let itemData = Zotero.Utilities.Item.itemToCSLJSON(parentItem);
if (!skipEmbeddingItemData) {
citationItem.itemData = itemData;
}
let item = storedCitationItems.find((item) =>
item.uris.some((uri) => uris.includes(uri))
);
if (!item) {
storedCitationItems.push({ uris, itemData });
}
storedAnnotation.citationItem = citationItem;
let citation = {
citationItems: [citationItem],
properties: {},
};
let citationWithData = JSON.parse(JSON.stringify(citation));
citationWithData.citationItems[0].itemData = itemData;
let formatted =
Zotero.EditorInstanceUtilities.formatCitation(citationWithData);
citationHTML = `<span class="citation" data-citation="${encodeURIComponent(
JSON.stringify(citation)
)}">${formatted}</span>`;
}
// Image
if (annotation.imageAttachmentKey) {
// Normalize image dimensions to 1.25 of the print size
let rect = annotation.position.rects[0];
let rectWidth = rect[2] - rect[0];
let rectHeight = rect[3] - rect[1];
// Constants from pdf.js
const CSS_UNITS = 96.0 / 72.0;
const PDFJS_DEFAULT_SCALE = 1.25;
let width = Math.round(rectWidth * CSS_UNITS * PDFJS_DEFAULT_SCALE);
let height = Math.round((rectHeight * width) / rectWidth);
imageHTML = `<img data-attachment-key="${
annotation.imageAttachmentKey
}" width="${width}" height="${height}" data-annotation="${encodeURIComponent(
JSON.stringify(storedAnnotation)
)}"/>`;
}
// Image in b64
if (annotation.image) {
imageHTML = `<img src="${annotation.image}"/>`;
}
// Text
if (annotation.text) {
let text = Zotero.EditorInstanceUtilities._transformTextToHTML.call(
Zotero.EditorInstanceUtilities,
annotation.text.trim()
);
highlightHTML = `<span class="highlight" data-annotation="${encodeURIComponent(
JSON.stringify(storedAnnotation)
)}">${text}</span>`;
quotedHighlightHTML = `<span class="highlight" data-annotation="${encodeURIComponent(
JSON.stringify(storedAnnotation)
)}">${Zotero.getString(
"punctuation.openingQMark"
)}${text}${Zotero.getString("punctuation.closingQMark")}</span>`;
}
// Note
if (annotation.comment) {
commentHTML = Zotero.EditorInstanceUtilities._transformTextToHTML.call(
Zotero.EditorInstanceUtilities,
annotation.comment.trim()
);
}
let template: string = "";
if (annotation.type === "highlight") {
template = Zotero.Prefs.get(
"annotations.noteTemplates.highlight"
) as string;
} else if (annotation.type === "note") {
template = Zotero.Prefs.get("annotations.noteTemplates.note") as string;
} else if (annotation.type === "image") {
template = "<p>{{image}}<br/>{{citation}} {{comment}}</p>";
}
ztoolkit.log("Using note template:");
ztoolkit.log(template);
template = template.replace(
/(<blockquote>[^<>]*?)({{highlight}})([\s\S]*?<\/blockquote>)/g,
(match, p1, p2, p3) => p1 + "{{highlight quotes='false'}}" + p3
);
let vars = {
color: annotation.color || "",
// Include quotation marks by default, but allow to disable with `quotes='false'`
highlight: (attrs: any) =>
attrs.quotes === "false" ? highlightHTML : quotedHighlightHTML,
comment: commentHTML,
citation: citationHTML,
image: imageHTML,
tags: (attrs: any) =>
(
(annotation.tags && annotation.tags.map((tag: any) => tag.name)) ||
[]
).join(attrs.join || " "),
};
let templateHTML = Zotero.Utilities.Internal.generateHTMLFromTemplate(
template,
vars
);
// Remove some spaces at the end of paragraph
templateHTML = templateHTML.replace(/([\s]*)(<\/p)/g, "$2");
// Remove multiple spaces
templateHTML = templateHTML.replace(/\s\s+/g, " ");
html += templateHTML;
}
return { html, citationItems: storedCitationItems };
}
export async function importAnnotationImagesToNote(
note: Zotero.Item | undefined,
annotations: CustomAnnotationJSON[]
) {
for (let annotation of annotations) {
if (annotation.image && note) {
annotation.imageAttachmentKey =
(await importImageToNote(note, annotation.image)) || "";
delete annotation.image;
}
}
}
export async function parseAnnotationHTML(
annotations: Zotero.Item[],
options: {
noteItem?: Zotero.Item; // If you are sure there are no image annotations, note is not required.
ignoreComment?: boolean;
skipCitation?: boolean;
} = {}
) {
let annotationJSONList: CustomAnnotationJSON[] = [];
for (const annot of annotations) {
const annotJson = await parseAnnotationJSON(annot);
if (options.ignoreComment && annotJson?.comment) {
annotJson.comment = "";
}
annotationJSONList.push(annotJson!);
}
await importAnnotationImagesToNote(options.noteItem, annotationJSONList);
const html = serializeAnnotations(
annotationJSONList as Required<CustomAnnotationJSON>[],
false,
options.skipCitation
).html;
return html;
}