zotero-better-notes/src/modules/export/api.ts

281 lines
8.2 KiB
TypeScript

import {
getLinkedNotesRecursively,
getNoteLink,
getNoteLinkParams,
} from "../../utils/link";
import { getString } from "../../utils/locale";
import { getLinesInNote } from "../../utils/note";
import { formatPath, jointPath, tryDecodeParse } from "../../utils/str";
export { exportNotes };
async function exportNotes(
noteItems: Zotero.Item[],
options: {
embedLink?: boolean;
standaloneLink?: boolean;
exportNote?: boolean;
exportMD?: boolean;
setAutoSync?: boolean;
autoMDFileName?: boolean;
syncDir?: string;
withYAMLHeader?: boolean;
exportDocx?: boolean;
exportPDF?: boolean;
exportFreeMind?: boolean;
},
) {
let inputNoteItems = noteItems;
// If embedLink or exportNote, create a new note item
if ((options.embedLink || options.exportNote) && !options.setAutoSync) {
inputNoteItems = [];
for (const noteItem of noteItems) {
const noteID = await ZoteroPane.newNote();
const newNote = Zotero.Items.get(noteID);
newNote.setNote(noteItem.getNote());
await newNote.saveTx({
skipSelect: true,
skipNotifier: true,
skipSyncedUpdate: true,
});
await Zotero.DB.executeTransaction(async () => {
await Zotero.Notes.copyEmbeddedImages(noteItem, newNote);
});
if (options.embedLink) {
newNote.setNote(await embedLinkedNotes(newNote));
}
await newNote.saveTx();
inputNoteItems.push(newNote);
}
}
let linkedNoteItems = [] as Zotero.Item[];
if (options.standaloneLink) {
const linkedNoteIds = [] as number[];
for (const noteItem of inputNoteItems) {
const linkedIds: number[] = getLinkedNotesRecursively(
getNoteLink(noteItem) || "",
linkedNoteIds,
);
linkedNoteIds.push(...linkedIds);
}
const targetNoteItemIds = inputNoteItems.map((item) => item.id);
linkedNoteItems = Zotero.Items.get(
linkedNoteIds.filter((id) => !targetNoteItemIds.includes(id)),
);
}
const allNoteItems = Array.from(
new Set(inputNoteItems.concat(linkedNoteItems)),
);
if (options.exportMD) {
if (options.setAutoSync) {
const raw = await new ztoolkit.FilePicker(
`${getString("fileInterface.sync")} MarkDown File`,
"folder",
).open();
if (raw) {
const syncDir = formatPath(raw);
// Hard reset sync status for input notes
for (const noteItem of inputNoteItems) {
await toSync(noteItem, syncDir, true);
}
// Find linked notes that are not synced and include them in sync
for (const noteItem of linkedNoteItems) {
await toSync(noteItem, syncDir, false);
}
await addon.hooks.onSyncing(allNoteItems, {
quiet: true,
skipActive: false,
reason: "export",
});
}
} else {
let exportDir: string | false = false;
if (options.autoMDFileName) {
const raw = await new ztoolkit.FilePicker(
`${getString("fileInterface.export")} MarkDown File`,
"folder",
).open();
exportDir = raw && formatPath(raw);
}
for (const noteItem of allNoteItems) {
await toMD(noteItem, {
filename:
(exportDir &&
jointPath(
exportDir,
await addon.api.sync.getMDFileName(noteItem.id, exportDir),
)) ||
undefined,
withYAMLHeader: options.withYAMLHeader,
keepNoteLink: true,
});
}
}
}
if (options.exportDocx) {
for (const noteItem of allNoteItems) {
await toDocx(noteItem);
}
}
if (options.exportFreeMind) {
for (const noteItem of allNoteItems) {
await toFreeMind(noteItem);
}
}
if (options.exportPDF) {
for (const noteItem of allNoteItems) {
await addon.api.$export.savePDF(noteItem.id);
}
}
if (options.embedLink && !options.exportNote) {
// If not exportNote, delete temp notes
for (const noteItem of allNoteItems) {
const _w: Window = ZoteroPane.findNoteWindow(noteItem.id);
if (_w) {
_w.close();
}
await Zotero.Items.erase(noteItem.id);
}
} else if (options.exportNote) {
for (const noteItem of allNoteItems) {
ZoteroPane.openNoteWindow(noteItem.id);
}
}
}
async function toMD(
noteItem: Zotero.Item,
options: {
filename?: string;
keepNoteLink?: boolean;
withYAMLHeader?: boolean;
} = {},
) {
let filename = options.filename;
if (!filename) {
const raw = await new ztoolkit.FilePicker(
`${Zotero.getString("fileInterface.export")} MarkDown File`,
"save",
[["MarkDown File(*.md)", "*.md"]],
`${noteItem.getNoteTitle()}.md`,
).open();
if (!raw) return;
filename = formatPath(raw, ".md");
}
await addon.api.$export.saveMD(filename, noteItem.id, options);
}
async function toSync(
noteItem: Zotero.Item,
syncDir: string,
overwrite: boolean = false,
) {
if (!overwrite && addon.api.sync.isSyncNote(noteItem.id)) {
return;
}
addon.api.sync.updateSyncStatus(noteItem.id, {
path: syncDir,
filename: await addon.api.sync.getMDFileName(noteItem.id, syncDir),
md5: "",
noteMd5: Zotero.Utilities.Internal.md5(noteItem.getNote(), false),
lastsync: 0,
itemID: noteItem.id,
});
}
async function toDocx(noteItem: Zotero.Item) {
const raw = await new ztoolkit.FilePicker(
`${Zotero.getString("fileInterface.export")} MS Word Docx`,
"save",
[["MS Word Docx File(*.docx)", "*.docx"]],
`${noteItem.getNoteTitle()}.docx`,
).open();
if (!raw) return;
const filename = formatPath(raw, ".docx");
await addon.api.$export.saveDocx(filename, noteItem.id);
}
async function toFreeMind(noteItem: Zotero.Item) {
const raw = await new ztoolkit.FilePicker(
`${Zotero.getString("fileInterface.export")} FreeMind XML`,
"save",
[["FreeMind XML File(*.mm)", "*.mm"]],
`${noteItem.getNoteTitle()}.mm`,
).open();
if (!raw) return;
const filename = formatPath(raw, ".mm");
await addon.api.$export.saveFreeMind(filename, noteItem.id);
}
async function embedLinkedNotes(noteItem: Zotero.Item): Promise<string> {
const parser = ztoolkit.getDOMParser();
const globalCitationData = getNoteCitationData(noteItem as Zotero.Item);
const newLines: string[] = [];
const noteLines = getLinesInNote(noteItem);
for (const i in noteLines) {
newLines.push(noteLines[i]);
const doc = parser.parseFromString(noteLines[i], "text/html");
const linkParams = Array.from(doc.querySelectorAll("a"))
.filter((a) => a.href.startsWith("zotero://note/"))
.map((a) => getNoteLinkParams(a.href))
.filter((p) => p.noteItem && !p.ignore);
for (const linkParam of linkParams) {
const html = await addon.api.template.runTemplate(
"[QuickImportV2]",
"link, noteItem",
[linkParam.link, noteItem],
);
newLines.push(html);
const citationData = getNoteCitationData(
linkParam.noteItem as Zotero.Item,
);
globalCitationData.items.push(...citationData.items);
}
}
// Clean up globalCitationItems
const seenCitationItemIDs = [] as string[];
const finalCitationItems = [];
for (const citationItem of globalCitationData.items) {
const currentID = citationItem.uris[0];
if (!(currentID in seenCitationItemIDs)) {
finalCitationItems.push(citationItem);
seenCitationItemIDs.push(currentID);
}
}
return `<div data-schema-version="${
globalCitationData.schemaVersion
}" data-citation-items="${encodeURIComponent(
JSON.stringify(finalCitationItems),
)}">${newLines.join("\n")}</div>`;
}
function getNoteCitationData(noteItem: Zotero.Item) {
const parser = new DOMParser();
const doc = parser.parseFromString(noteItem.getNote(), "text/html");
const citationItems = tryDecodeParse(
doc
.querySelector("div[data-citation-items]")
?.getAttribute("data-citation-items") || "[]",
) as unknown as Array<{
uris: string[];
itemData: Record<string, any>;
schemaVersion: string;
}>;
const citationData = {
items: citationItems,
schemaVersion:
doc
.querySelector("div[data-schema-version]")
?.getAttribute("data-schema-version") || "",
};
return citationData;
}