import { ZoteroAPI } from "@clients/zotero";
import { findRoamBlock, findRoamPage, makeDNP } from "@services/roam";
import { cleanNewlines, compareAnnotationIndices, executeFunctionByName, extractSortIndex, formatItemAnnotations, formatZoteroNotes, getLocalLink, getPDFLink, getWebLink, simplifyZoteroAnnotations } from "../utils";
import { SettingsAnnotations, SettingsNotes, SettingsTypemap } from "Types/extension";
import { AsBoolean } from "Types/helpers";
import { RImportableElement, ZItem, ZItemAnnotation, ZItemAttachment, ZItemNote, ZItemTop, ZLinkType, ZLinkOptions, isZAnnotation, isZNote } from "Types/transforms";
/** Parses the XHTML bibliography for a Zotero item into Roam formatting
* @param bib - The item's XHTML bibliography
* @returns The clean bibliography string
*/
function cleanBibliographyHTML(bib: string) {
let bibString = bib;
// Strip divs
const richTags = ["div"];
richTags.forEach(tag => {
// eslint-disable-next-line no-useless-escape
const tagRegex = new RegExp(`<\/?${tag}>|<${tag} .+?>`, "g"); // Covers both the simple case : or , and the case with modifiers :
bibString = bibString.replaceAll(tagRegex, "");
});
bibString = cleanNewlines(bibString).trim();
// Use a textarea element to decode HTML
const formatter = document.createElement("textarea");
formatter.innerHTML = bibString;
let formattedBib = formatter.innerText;
// Convert italics
formattedBib = formattedBib.replaceAll(/<\/?i>/g, "__");
// Convert links
const linkRegex = /(.+)<\/a>/g;
formattedBib = formattedBib.replaceAll(linkRegex, "[$2]($1)");
return formattedBib;
}
/** Orders the (raw) indices of two Zotero annotations */
function compareAnnotationRawIndices(a: string, b: string): number {
return compareAnnotationIndices(
extractSortIndex(a),
extractSortIndex(b)
);
}
/* istanbul ignore next */
/** Formats an item's and its children's metadata for import to Roam using the default template */
function formatItemMetadata(
item: ZItemTop, pdfs: ZItemAttachment[], notes: (ZItemNote | ZItemAnnotation)[],
{ annotationsSettings, notesSettings, typemap }: { annotationsSettings: SettingsAnnotations, notesSettings: SettingsNotes, typemap: SettingsTypemap }
): RImportableElement[] {
const metadata: RImportableElement[] = [];
if (item.data.title) { metadata.push(`Title:: ${item.data.title}`); } // Title, if available
if (item.data.creators.length > 0) { metadata.push(`Author(s):: ${getItemCreators(item, { return_as: "string", brackets: true, use_type: true })}`); } // Creators list, if available
if (item.data.abstractNote) { metadata.push(`Abstract:: ${item.data.abstractNote}`); } // Abstract, if available
if (item.data.itemType) { metadata.push(`Type:: ${getItemType(item, { brackets: true }, { typemap })}`); } // Item type, according to typemap
metadata.push(`Publication:: ${getItemPublication(item, { brackets: true })}`);
if (item.data.url) { metadata.push(`URL : ${item.data.url}`); }
if (item.data.dateAdded) { metadata.push(`Date Added:: ${getItemDateAdded(item)}`); } // Date added, as Daily Notes Page reference
metadata.push(`Zotero links:: ${getItemLink(item, "local", { format: "markdown", text: "Local library" })}, ${getItemLink(item, "web", { format: "markdown", text: "Web library" })}`); // Local + Web links to the item
if (item.data.tags.length > 0) { metadata.push(`Tags:: ${getItemTags(item, { return_as: "string", brackets: true })}`); } // Tags, if any
if (pdfs.length > 0) {
metadata.push(`PDF links : ${(formatPDFs(pdfs, "links") as string[]).join(", ")}`);
}
if (notes.length > 0) {
const formattedOutput = formatNotes(notes, null, { annotationsSettings, notesSettings });
metadata.push(...formattedOutput);
}
return metadata;
}
/** Formats Zotero notes/annotations items */
function formatNotes(
/** The Array of Zotero notes/annotations */
notes: (ZItemNote | ZItemAnnotation)[],
/** The UID of the parent item's Roam page, if it exists */
pageUID: string | null = null,
/** The user's current settings */
{ annotationsSettings, notesSettings }: { annotationsSettings: SettingsAnnotations, notesSettings: SettingsNotes }
): RImportableElement[] {
if (!notes || notes.length == 0) {
return [];
} else {
const annotItems = notes
.filter(isZAnnotation)
.sort((a, b) => compareAnnotationRawIndices(a.data.annotationSortIndex, b.data.annotationSortIndex));
const noteItems = notes
.filter(isZNote)
.sort((a, b) => a.data.dateAdded < b.data.dateAdded ? -1 : 1);
const formattedOutput = [
...formatZoteroAnnotations(annotItems, annotationsSettings),
...formatZoteroNotes(noteItems, notesSettings)
];
const { nest_char, nest_position, nest_preset, nest_use } = notesSettings;
// If nesting is disabled, simply return the array of blocks
if (nest_use == "preset" && !nest_preset) {
return formattedOutput;
}
// Else if the page UID was provided, check if the page already has a block with the same content
// If yes, set that block as the parent for all the outputted blocks
if (pageUID) {
const blockString = ((nest_use == "custom") ? nest_char : nest_preset) || "";
const existingBlock = findRoamBlock(blockString, pageUID);
if (existingBlock) {
const { uid, children = [] } = existingBlock;
const pos = (nest_position == "bottom") ? children.length : 0;
return formattedOutput.map(blck => {
if (blck.constructor === String) {
return {
string: blck,
text: blck,
order: pos,
parentUID: uid
};
} else {
return {
...blck,
order: pos,
parentUID: uid
};
}
});
}
}
const blockString = (nest_use == "custom" ? nest_char : nest_preset) || "";
return [{
string: blockString,
text: blockString,
children: formattedOutput
}];
}
}
type PDFFormatOption = "links" | "identity" | "string";
type PDFAsIdentity = { key: string, link: string, title: string };
/** Converts Zotero PDF items into a specific format */
function formatPDFs(pdfs: ZItemAttachment[], as: PDFFormatOption = "string"): string | string[] | PDFAsIdentity[] {
if (!pdfs) {
switch (as) {
case "identity":
case "links":
return [];
case "string":
default:
return "";
}
} else {
const pdfsIdentity = pdfs.map(file => ({
title: file.data.filename || file.data.title,
key: file.key,
link: getPDFLink(file, "href")
}));
switch (as) {
case "identity":
return pdfsIdentity;
case "links":
return pdfsIdentity.map(entry => `[${entry.title}](${entry.link})`);
case "string":
default:
return pdfsIdentity.map(entry => `[${entry.title}](${entry.link})`).join(", ");
}
}
}
type CreatorAsIdentity = { inGraph: string | boolean, name: string, type: ZoteroAPI.CreatorType };
type CreatorOptions = { brackets: boolean | "existing", return_as: "array" | "identity" | "string", use_type: boolean };
/** Retrieves the creators list of a Zotero item, and returns it into a specific format */
function getItemCreators(item: ZItemTop, { return_as = "string", brackets = true, use_type = true }: Partial = {}): string | string[] | CreatorAsIdentity[] {
const creatorsInfoList = item.data.creators.map(creator => {
const nameTag = "name" in creator
? creator.name
: `${[creator.firstName, creator.lastName].filter(AsBoolean).join(" ")}`;
return {
name: nameTag,
type: creator.creatorType,
inGraph: findRoamPage(nameTag)
};
});
switch (return_as) {
case "identity":
return creatorsInfoList;
case "array":
return creatorsInfoList.map(c => c.name);
case "string":
default:
return creatorsInfoList.map(creator => {
const creatorTag = (brackets == true || (brackets == "existing" && creator.inGraph))
? `[[${creator.name}]]`
: creator.name;
return (use_type == true ? creatorTag + (creator.type == "author" ? "" : ` (${creator.type})`) : creatorTag);
}).join(", ");
}
}
type TagOptions = { brackets: boolean, return_as: "array" | "string" };
/** Retrieves the tags of a Zotero item, and returns them into a specific format */
function getItemTags(item: ZItemTop, { return_as = "string", brackets = true }: Partial = {}): string | string[] {
const tags = item.data.tags.map(t => t.tag);
const tagList = (brackets == true ? tags.map(el => `#[[${el}]]`) : tags);
switch (return_as) {
case "array":
return tagList;
case "string":
default:
return tagList.join(" ");
}
}
/** Formats an array of Zotero annotations into Roam blocks, with optional configuration */
function formatZoteroAnnotations(
annotations: ZItemAnnotation[],
{ func = "", use = "default", __with = "raw", ...settings }: Partial = {}
): any {
if (use == "function" && func) {
// If the user has provided a custom function, execute it with the desired input
return executeFunctionByName(func, window, __with == "raw" ? annotations : simplifyZoteroAnnotations(annotations));
} else {
return formatItemAnnotations(annotations, { ...settings });
}
}
/** Returns the date on which an item was added to Zotero, in DNP format */
function getItemDateAdded(item: ZItem, { brackets = true }: { brackets?: boolean } = {}): string {
return makeDNP(item.data.dateAdded, { brackets });
}
/** Returns a link for the item (web or local) */
function getItemLink(item: ZItemTop, type: ZLinkType = "local", config: Partial = {}) {
return (type == "local")
? getLocalLink(item, config)
: getWebLink(item, config);
}
/** Retrieves the publication details for a given item.
* The extension will check for the existence of a `publicationTitle`, then a `bookTitle`, then a `university` name.
*/
function getItemPublication(item: ZItemTop, { brackets = true }: { brackets?: boolean } = {}): string {
const maybePublication = item.data.publicationTitle || item.data.bookTitle || item.data.university;
if (maybePublication) {
return (brackets == true)
? `[[${maybePublication}]]`
: maybePublication;
} else {
return "";
}
}
/** Retrieves the type of a Zotero item, according to a given typemap */
function getItemType(item: ZItemTop, { brackets = true }: { brackets?: boolean } = {}, { typemap }: { typemap: SettingsTypemap }): string {
const type = typemap[item.data.itemType] || item.data.itemType;
return (brackets == true ? `[[${type}]]` : type);
}
/** Groups a list of citekeys by the library they belong to. */
function groupCitekeysByLibrary(citekeys: string[], { items }: { items: ZItem[] }) {
return citekeys.reduce>((obj, citekey) => {
const libItem = items.find(it => it.key == citekey);
if (libItem) {
const location = libItem.library.type + "s/" + libItem.library.id;
if (Object.keys(obj).includes(location)) {
obj[location].push(libItem.data.key);
} else {
obj[location] = [libItem.data.key];
}
}
return obj;
}, {});
}
export {
cleanBibliographyHTML,
compareAnnotationRawIndices,
formatItemMetadata,
formatNotes,
formatPDFs,
formatZoteroAnnotations,
getItemCreators,
getItemDateAdded,
getItemLink,
getItemPublication,
getItemTags,
getItemType,
groupCitekeysByLibrary
};