import YAML = require("yamljs"); import { getPref, setPref } from "../../utils/prefs"; import { config } from "../../../package.json"; import { fileExists, formatPath, jointPath } from "../../utils/str"; export { initSyncList, removeSyncNote, isSyncNote, getSyncNoteIds, addSyncNote, updateSyncStatus, getSyncStatus, getNoteStatus, getMDStatus, getMDStatusFromContent, getMDFileName, findAllSyncedFiles, }; function initSyncList() { const rawKeys = getPref("syncNoteIds") as string; if (!rawKeys.startsWith("[") || !rawKeys.endsWith("]")) { const keys = rawKeys.split(",").map((id) => String(id)); setPref("syncNoteIds", JSON.stringify(keys)); } addon.data.sync.data = new ztoolkit.LargePref( `${config.prefsPrefix}.syncNoteIds`, `${config.prefsPrefix}.syncDetail-`, "parser", ); // Due to the bug in v1.1.4-22, the sync data may be corrupted const keys = addon.data.sync.data?.getKeys().map((key) => String(key)); setPref("syncNoteIds", JSON.stringify(keys)); } async function getSyncNoteIds() { const keys = addon.data.sync.data ?.getKeys() .map((key) => Number(key)) .filter((key) => !!key); if (!keys) { return []; } return (await Zotero.Items.getAsync(keys)) .filter((item) => item.isNote()) .map((item) => item.id); } function isSyncNote(noteId: number): boolean { return !!addon.data.sync.data?.hasKey(String(noteId)); } function addSyncNote(noteId: number) { addon.data.sync.data?.setKey(String(noteId)); } function removeSyncNote(noteId: number) { addon.data.sync.data?.deleteKey(String(noteId)); } function updateSyncStatus(noteId: number, status: SyncStatus) { addon.data.sync.data?.setValue(String(noteId), status); } function getNoteStatus(noteId: number) { const noteItem = Zotero.Items.get(noteId); if (!noteItem?.isNote()) { return; } const fullContent = noteItem.getNote(); const ret = { meta: "", content: "", tail: "", lastmodify: Zotero.Date.sqlToDate(noteItem.dateModified, true), }; const metaRegex = /^]*>/; // Not wrapped inside div if (!metaRegex.test(fullContent)) { ret.meta = `
`; ret.content = fullContent || ""; return ret; } const metaMatch = fullContent.match(metaRegex); ret.meta = metaMatch ? metaMatch[0] : ""; ret.content = fullContent.substring( ret.meta.length, fullContent.length - ret.tail.length, ); return ret; } function getSyncStatus(noteId?: number): SyncStatus { const defaultStatus = { path: "", filename: "", md5: "", noteMd5: "", lastsync: new Date().getTime(), itemID: -1, }; const status = { ...defaultStatus, ...(addon.data.sync.data?.getValue(String(noteId)) as SyncStatus), }; status.path = formatPath(status.path); return status; } function getMDStatusFromContent(contentRaw: string): MDStatus { contentRaw = contentRaw.replace(/\r\n/g, "\n"); const result = contentRaw.match(/^---\n(.*\n)+?---$/gm); const ret: MDStatus = { meta: { $version: -1 }, content: contentRaw, filedir: "", filename: "", lastmodify: new Date(0), }; if (result) { const yaml = result[0].replace(/---/g, ""); ret.content = contentRaw.slice(result[0].length); try { ret.meta = YAML.parse(yaml); } catch (e) { ztoolkit.log(e); } } return ret; } async function getMDStatus( source: Zotero.Item | number | string, ): Promise { let ret: MDStatus = { meta: null, content: "", filedir: "", filename: "", lastmodify: new Date(0), }; try { let filepath = ""; if (typeof source === "string") { filepath = source; } else if (typeof source === "number") { const syncStatus = getSyncStatus(source); filepath = jointPath(syncStatus.path, syncStatus.filename); } else if (source.isNote && source.isNote()) { const syncStatus = getSyncStatus(source.id); filepath = jointPath(syncStatus.path, syncStatus.filename); } filepath = formatPath(filepath); if (await fileExists(filepath)) { const contentRaw = (await Zotero.File.getContentsAsync( filepath, "utf-8", )) as string; ret = getMDStatusFromContent(contentRaw); const pathSplit = PathUtils.split(filepath); ret.filedir = formatPath(pathSplit.slice(0, -1).join("/")); ret.filename = pathSplit.pop() || ""; const stat = await IOUtils.stat(filepath); ret.lastmodify = new Date(stat.lastModified || 0); } } catch (e) { ztoolkit.log(e); } return ret; } async function getMDFileName(noteId: number, searchDir?: string) { const syncStatus = getSyncStatus(noteId); // If the note is already synced, use the filename in sync status if ( (!searchDir || searchDir === syncStatus.path) && syncStatus.filename && (await fileExists(jointPath(syncStatus.path, syncStatus.filename))) ) { return syncStatus.filename; } // If the note is not synced or the synced file does not exists, search for the latest file with the same key const noteItem = Zotero.Items.get(noteId); if (searchDir !== undefined && (await fileExists(searchDir))) { const mdRegex = /\.(md|MD|Md|mD)$/; let matchedFileName = null; let matchedDate = 0; await Zotero.File.iterateDirectory( searchDir, async (entry: OS.File.Entry) => { if (entry.isDir) return; if (mdRegex.test(entry.name)) { if ( entry.name.split(".").shift()?.split("-").pop() === noteItem.key ) { const stat = await IOUtils.stat(entry.path); if (stat.lastModified || 0 > matchedDate) { matchedFileName = entry.name; matchedDate = stat.lastModified || 0; } } } }, ); if (matchedFileName) { return matchedFileName; } } // If no file found, use the template to generate a new filename return await addon.api.template.runTemplate( "[ExportMDFileNameV2]", "noteItem", [noteItem], ); } async function findAllSyncedFiles(searchDir: string) { const results: SyncStatus[] = []; const mdRegex = /\.(md|MD|Md|mD)$/; await Zotero.File.iterateDirectory( searchDir, async (entry: OS.File.Entry) => { if (entry.isDir) { const subDirResults = await findAllSyncedFiles(entry.path); results.push(...subDirResults); return; } if (mdRegex.test(entry.name)) { const MDStatus = await getMDStatus(entry.path); if (!MDStatus.meta?.$libraryID || !MDStatus.meta?.$itemKey) { return; } const item = await Zotero.Items.getByLibraryAndKeyAsync( MDStatus.meta.$libraryID, MDStatus.meta.$itemKey, ); if (!item || !(item as Zotero.Item).isNote()) { return; } results.push({ path: MDStatus.filedir, filename: MDStatus.filename, md5: Zotero.Utilities.Internal.md5(MDStatus.content, false), noteMd5: Zotero.Utilities.Internal.md5( (item as Zotero.Item).getNote(), false, ), lastsync: MDStatus.lastmodify.getTime(), itemID: item.id, }); } }, ); return results; }