and wrapped by
+ visitParents(
+ rehype,
+ (_n: any) =>
+ _n.type === "element" && (_n.tagName === "span" || _n.tagName === "img"),
+ (_n: any, ancestors) => {
+ if (ancestors.length) {
+ const parentNode = ancestors[ancestors.length - 1];
+ if (parentNode === rehype) {
+ const newChild = h("span");
+ replace(newChild, _n);
+ const p = h("p", [newChild]);
+ replace(_n, p);
+ }
+ }
+ }
+ );
+
+ // Make sure empty
under root node is removed
+ visitParents(
+ rehype,
+ (_n: any) => _n.type === "element" && _n.tagName === "p",
+ (_n: any, ancestors) => {
+ if (ancestors.length) {
+ const parentNode = ancestors[ancestors.length - 1];
+ if (parentNode === rehype && !_n.children.length && !toText(_n)) {
+ parentNode.children.splice(parentNode.children.indexOf(_n), 1);
+ }
+ }
+ }
+ );
+ return rehype;
+}
+
+async function rehype2remark(rehype: HRoot) {
+ return await unified()
+ .use(rehypeRemark, {
+ handlers: {
+ span: (h, node) => {
+ if (
+ node.properties?.style?.includes("text-decoration: line-through")
+ ) {
+ return h(node, "delete", all(h, node));
+ } else if (node.properties?.style?.includes("background-color")) {
+ return h(node, "html", toHtml(node));
+ } else if (node.properties?.className?.includes("math")) {
+ return h(node, "inlineMath", toText(node).slice(1, -1));
+ } else {
+ return h(node, "paragraph", all(h, node));
+ }
+ },
+ pre: (h, node) => {
+ if (node.properties?.className?.includes("math")) {
+ return h(node, "math", toText(node).slice(2, -2));
+ } else {
+ return h(node, "code", toText(node));
+ }
+ },
+ u: (h, node) => {
+ return h(node, "u", toText(node));
+ },
+ sub: (h, node) => {
+ return h(node, "sub", toText(node));
+ },
+ sup: (h, node) => {
+ return h(node, "sup", toText(node));
+ },
+ table: (h, node) => {
+ let hasStyle = false;
+ visit(
+ node,
+ (_n) =>
+ _n.type === "element" &&
+ ["tr", "td", "th"].includes((_n as any).tagName),
+ (node) => {
+ if (node.properties.style) {
+ hasStyle = true;
+ }
+ }
+ );
+ if (0 && hasStyle) {
+ return h(node, "styleTable", toHtml(node));
+ } else {
+ return defaultHandlers.table(h, node);
+ }
+ },
+ wrapper: (h, node) => {
+ return h(node, "wrapper", toText(node));
+ },
+ wrapperleft: (h, node) => {
+ return h(node, "wrapperleft", toText(node));
+ },
+ wrapperright: (h, node) => {
+ return h(node, "wrapperright", toText(node));
+ },
+ zhighlight: (h, node) => {
+ return h(node, "zhighlight", toHtml(node));
+ },
+ zcitation: (h, node) => {
+ return h(node, "zcitation", toHtml(node));
+ },
+ znotelink: (h, node) => {
+ return h(node, "znotelink", toHtml(node));
+ },
+ zimage: (h, node) => {
+ return h(node, "zimage", toHtml(node));
+ },
+ },
+ })
+ .run(rehype);
+}
+
+function remark2md(remark: MRoot) {
+ return String(
+ unified()
+ .use(remarkGfm)
+ .use(remarkMath)
+ .use(remarkStringify, {
+ handlers: {
+ pre: (node: { value: string }) => {
+ return "```\n" + node.value + "\n```";
+ },
+ u: (node: { value: string }) => {
+ return "" + node.value + " ";
+ },
+ sub: (node: { value: string }) => {
+ return "" + node.value + " ";
+ },
+ sup: (node: { value: string }) => {
+ return "" + node.value + " ";
+ },
+ styleTable: (node: { value: any }) => {
+ return node.value;
+ },
+ wrapper: (node: { value: string }) => {
+ return "\n\n";
+ },
+ wrapperleft: (node: { value: string }) => {
+ return "\n";
+ },
+ wrapperright: (node: { value: string }) => {
+ return "\n";
+ },
+ zhighlight: (node: { value: string }) => {
+ return node.value.replace(/(^|<\/zhighlight>$)/g, "");
+ },
+ zcitation: (node: { value: string }) => {
+ return node.value.replace(/(^|<\/zcitation>$)/g, "");
+ },
+ znotelink: (node: { value: string }) => {
+ return node.value.replace(/(^|<\/znotelink>$)/g, "");
+ },
+ zimage: (node: { value: string }) => {
+ return node.value.replace(/(^|<\/zimage>$)/g, "");
+ },
+ },
+ } as any)
+ .stringify(remark)
+ );
+}
+
+function md2remark(str: string) {
+ // Parse Obsidian-style image ![[xxx.png]]
+ // Encode spaces in link, otherwise it cannot be parsed to image node
+ str = str
+ .replace(/!\[\[(.*)\]\]/g, (s: string) => `})`)
+ .replace(
+ /!\[.*\]\((.*)\)/g,
+ (s: string) =>
+ `/g)![0].slice(1, -1))})`
+ );
+ const remark = unified()
+ .use(remarkGfm)
+ .use(remarkMath)
+ .use(remarkParse)
+ .parse(str);
+ // visit(
+ // remark,
+ // (_n) => _n.type === "image",
+ // (_n: any) => {
+ // _n.type = "html";
+ // _n.value = toHtml(
+ // h("img", {
+ // src: _n.url,
+ // })
+ // );
+ // }
+ // );
+ return remark;
+}
+
+async function remark2rehype(remark: any) {
+ return await unified()
+ .use(remarkRehype, {
+ allowDangerousHtml: true,
+ })
+ .run(remark);
+}
+
+function rehype2note(rehype: HRoot) {
+ // Del node
+ visit(
+ rehype,
+ (node: any) => node.type === "element" && (node as any).tagName === "del",
+ (node: any) => {
+ node.tagName = "span";
+ node.properties.style = "text-decoration: line-through";
+ }
+ );
+
+ // Code node
+ visitParents(
+ rehype,
+ (node: any) => node.type === "element" && (node as any).tagName === "code",
+ (node: any, ancestors) => {
+ const parent = ancestors.length
+ ? ancestors[ancestors.length - 1]
+ : undefined;
+ if (parent?.type == "element" && parent?.tagName === "pre") {
+ node.value = toText(node);
+ node.type = "text";
+ }
+ }
+ );
+
+ // Table node with style
+ visit(
+ rehype,
+ (node: any) => node.type === "element" && (node as any).tagName === "table",
+ (node: any) => {
+ let hasStyle = false;
+ visit(
+ node,
+ (_n: any) =>
+ _n.type === "element" &&
+ ["tr", "td", "th"].includes((_n as any).tagName),
+ (node: any) => {
+ if (node.properties.style) {
+ hasStyle = true;
+ }
+ }
+ );
+ if (hasStyle) {
+ node.value = toHtml(node).replace(/[\r\n]/g, "");
+ node.children = [];
+ node.type = "raw";
+ }
+ }
+ );
+
+ // Convert thead to tbody
+ visit(
+ rehype,
+ (node: any) => node.type === "element" && (node as any).tagName === "thead",
+ (node: any) => {
+ node.value = toHtml(node).slice(7, -8);
+ node.children = [];
+ node.type = "raw";
+ }
+ );
+
+ // Wrap lines in list with (for diff)
+ visitParents(rehype, "text", (node: any, ancestors) => {
+ const parent = ancestors.length
+ ? ancestors[ancestors.length - 1]
+ : undefined;
+ if (
+ node.value.replace(/[\r\n]/g, "") &&
+ parent?.type == "element" &&
+ ["li", "td"].includes(parent?.tagName)
+ ) {
+ node.type = "element";
+ node.tagName = "p";
+ node.children = [
+ { type: "text", value: node.value.replace(/[\r\n]/g, "") },
+ ];
+ node.value = undefined;
+ }
+ });
+
+ // No empty breakline text node in list (for diff)
+ visit(
+ rehype,
+ (node: any) =>
+ node.type === "element" &&
+ ((node as any).tagName === "li" || (node as any).tagName === "td"),
+ (node: any) => {
+ node.children = node.children.filter(
+ (_n: { type: string; value: string }) =>
+ _n.type === "element" ||
+ (_n.type === "text" && _n.value.replace(/[\r\n]/g, ""))
+ );
+ }
+ );
+
+ // Math node
+ visit(
+ rehype,
+ (node: any) =>
+ node.type === "element" &&
+ ((node as any).properties?.className?.includes("math-inline") ||
+ (node as any).properties?.className?.includes("math-display")),
+ (node: any) => {
+ if (node.properties.className.includes("math-inline")) {
+ node.children = [
+ { type: "text", value: "$" },
+ ...node.children,
+ { type: "text", value: "$" },
+ ];
+ } else if (node.properties.className.includes("math-display")) {
+ node.children = [
+ { type: "text", value: "$$" },
+ ...node.children,
+ { type: "text", value: "$$" },
+ ];
+ node.tagName = "pre";
+ }
+ node.properties.className = "math";
+ }
+ );
+
+ // Ignore link rel attribute, which exists in note
+ visit(
+ rehype,
+ (node: any) => node.type === "element" && (node as any).tagName === "a",
+ (node: any) => {
+ node.properties.rel = undefined;
+ }
+ );
+
+ // Ignore empty lines, as they are not parsed to md
+ const tempChildren = [];
+ const isEmptyNode = (_n: Node) =>
+ (_n.type === "text" && !_n.value.trim()) ||
+ (_n.type === "element" &&
+ _n.tagName === "p" &&
+ !_n.children.length &&
+ !toText(_n).trim());
+ for (const child of rehype.children) {
+ if (
+ tempChildren.length &&
+ isEmptyNode(tempChildren[tempChildren.length - 1]) &&
+ isEmptyNode(child)
+ ) {
+ continue;
+ }
+ tempChildren.push(child);
+ }
+
+ rehype.children = tempChildren;
+
+ return unified()
+ .use(rehypeStringify, {
+ allowDangerousCharacters: true,
+ allowDangerousHtml: true,
+ })
+ .stringify(rehype);
+}
+
+async function rehype2rehype(rehype: HRoot) {
+ return await unified().use(rehypeFormat).run(rehype);
+}
+
+function replace(targetNode: any, sourceNode: any) {
+ targetNode.type = sourceNode.type;
+ targetNode.tagName = sourceNode.tagName;
+ targetNode.properties = sourceNode.properties;
+ targetNode.value = sourceNode.value;
+ targetNode.children = sourceNode.children;
+}
+
+function getN2MRehypeHighlightNodes(rehype: HRoot) {
+ const nodes: any[] | null | undefined = [];
+ visit(
+ rehype,
+ (node: any) =>
+ node.type === "element" &&
+ node.properties?.className?.includes("highlight"),
+ (node) => nodes.push(node)
+ );
+ return new Array(...new Set(nodes));
+}
+
+function getN2MRehypeCitationNodes(rehype: HRoot) {
+ const nodes: any[] | null | undefined = [];
+ visit(
+ rehype,
+ (node: any) =>
+ node.type === "element" &&
+ node.properties?.className?.includes("citation"),
+ (node) => nodes.push(node)
+ );
+ return new Array(...new Set(nodes));
+}
+
+function getN2MRehypeNoteLinkNodes(rehype: any) {
+ const nodes: any[] | null | undefined = [];
+ visit(
+ rehype,
+ (node: any) =>
+ node.type === "element" &&
+ node.tagName === "a" &&
+ node.properties?.href &&
+ /zotero:\/\/note\/\w+\/\w+\//.test(node.properties?.href),
+ (node) => nodes.push(node)
+ );
+ return new Array(...new Set(nodes));
+}
+
+function getN2MRehypeImageNodes(rehype: any) {
+ const nodes: any[] = [];
+ visit(
+ rehype,
+ (node: any) =>
+ node.type === "element" &&
+ node.tagName === "img" &&
+ node.properties?.dataAttachmentKey,
+ (node) => nodes.push(node)
+ );
+ return new Array(...new Set(nodes));
+}
+
+function processN2MRehypeHighlightNodes(
+ nodes: string | any[],
+ mode: NodeMode = NodeMode.default
+) {
+ if (!nodes.length) {
+ return;
+ }
+ for (const node of nodes) {
+ let annotation;
+ try {
+ annotation = JSON.parse(
+ decodeURIComponent(node.properties.dataAnnotation)
+ );
+ } catch (e) {
+ continue;
+ }
+ if (!annotation) {
+ continue;
+ }
+ // annotation.uri was used before note-editor v4
+ let uri = annotation.attachmentURI || annotation.uri;
+ let position = annotation.position;
+
+ if (typeof uri === "string" && typeof position === "object") {
+ let openURI;
+ let uriParts = uri.split("/");
+ let libraryType = uriParts[3];
+ let key = uriParts[uriParts.length - 1];
+ if (libraryType === "users") {
+ openURI = "zotero://open-pdf/library/items/" + key;
+ }
+ // groups
+ else {
+ let groupID = uriParts[4];
+ openURI = "zotero://open-pdf/groups/" + groupID + "/items/" + key;
+ }
+
+ openURI +=
+ "?page=" +
+ (position.pageIndex + 1) +
+ (annotation.annotationKey
+ ? "&annotation=" + annotation.annotationKey
+ : "");
+
+ let newNode = h("span", [
+ h(node.tagName, node.properties, node.children),
+ h("span", " ("),
+ h("a", { href: openURI }, ["pdf"]),
+ h("span", ") "),
+ ]);
+ const annotKey =
+ annotation.annotationKey ||
+ randomString(
+ 8,
+ Zotero.Utilities.Internal.md5(node.properties.dataAnnotation),
+ Zotero.Utilities.allowedKeyChars
+ );
+
+ if (mode === NodeMode.wrap) {
+ newNode.children.splice(0, 0, h("wrapperleft", `annot:${annotKey}`));
+ newNode.children.push(h("wrapperright", `annot:${annotKey}`));
+ } else if (mode === NodeMode.replace) {
+ newNode = h("placeholder", `annot:${annotKey}`);
+ } else if (mode === NodeMode.direct) {
+ const newChild = h("span") as any;
+ replace(newChild, node);
+ newChild.children = [h("a", { href: openURI }, node.children)];
+ newChild.properties.ztype = "zhighlight";
+ newNode = h("zhighlight", [newChild]);
+ }
+ replace(node, newNode);
+ }
+ }
+}
+
+function processN2MRehypeCitationNodes(
+ nodes: string | any[],
+ mode: NodeMode = NodeMode.default
+) {
+ if (!nodes.length) {
+ return;
+ }
+ for (const node of nodes) {
+ let citation;
+ try {
+ citation = JSON.parse(decodeURIComponent(node.properties.dataCitation));
+ } catch (e) {
+ continue;
+ }
+ if (!citation?.citationItems?.length) {
+ continue;
+ }
+
+ let uris: any[] = [];
+ for (let citationItem of citation.citationItems) {
+ let uri = citationItem.uris[0];
+ if (typeof uri === "string") {
+ let uriParts = uri.split("/");
+ let libraryType = uriParts[3];
+ let key = uriParts[uriParts.length - 1];
+ if (libraryType === "users") {
+ uris.push("zotero://select/library/items/" + key);
+ }
+ // groups
+ else {
+ let groupID = uriParts[4];
+ uris.push("zotero://select/groups/" + groupID + "/items/" + key);
+ }
+ }
+ }
+
+ let childNodes: any[] = [];
+
+ visit(
+ node,
+ (_n: any) => _n.properties?.className.includes("citation-item"),
+ (_n: any) => {
+ return childNodes?.push(_n);
+ }
+ );
+
+ // For unknown reasons, the element will be duplicated. Remove them.
+ childNodes = new Array(...new Set(childNodes));
+
+ // Fallback to pre v5 note-editor schema that was serializing citations as plain text i.e.:
+ // (Jang et al., 2005, p. 14; Kongsgaard et al., 2009, p. 790)
+ if (!childNodes.length) {
+ childNodes = toText(node).slice(1, -1).split("; ");
+ }
+
+ let newNode = h("span", node.properties, [
+ { type: "text", value: "(" },
+ ...childNodes.map((child, i) => {
+ const newNode = h("span");
+ replace(newNode, child);
+ newNode.children = [h("a", { href: uris[i] }, child.children)];
+ return newNode;
+ }),
+ { type: "text", value: ")" },
+ ]);
+ const citationKey = randomString(
+ 8,
+ Zotero.Utilities.Internal.md5(node.properties.dataCitation),
+ Zotero.Utilities.allowedKeyChars
+ );
+ if (mode === NodeMode.wrap) {
+ newNode.children.splice(0, 0, h("wrapperleft", `cite:${citationKey}`));
+ newNode.children.push(h("wrapperright", `cite:${citationKey}`));
+ } else if (mode === NodeMode.replace) {
+ newNode = h("placeholder", `cite:${citationKey}`);
+ } else if (mode === NodeMode.direct) {
+ const newChild = h("span") as any;
+ replace(newChild, newNode);
+ newChild.properties.ztype = "zcitation";
+ newNode = h("zcitation", [newChild]);
+ }
+ replace(node, newNode);
+ }
+}
+
+async function processN2MRehypeNoteLinkNodes(
+ nodes: string | any[],
+ dir: string,
+ mode: NodeMode = NodeMode.default
+) {
+ if (!nodes.length) {
+ return;
+ }
+ for (const node of nodes) {
+ const linkParam = getNoteLinkParams(node.properties.href);
+ if (!linkParam.noteItem) {
+ continue;
+ }
+ const link =
+ mode === NodeMode.default ||
+ !addon.api.sync.isSyncNote(linkParam.noteItem.id)
+ ? node.properties.href
+ : `./${await addon.api.sync.getMDFileName(linkParam.noteItem.id, dir)}`;
+ const linkKey = randomString(
+ 8,
+ Zotero.Utilities.Internal.md5(node.properties.href),
+ Zotero.Utilities.allowedKeyChars
+ );
+ if (mode === NodeMode.wrap) {
+ const newNode = h("span", [
+ h("wrapperleft", `note:${linkKey}`),
+ h(
+ node.tagName,
+ Object.assign(node.properties, { href: link }),
+ node.children
+ ),
+ h("wrapperright", `note:${linkKey}`),
+ ]);
+ replace(node, newNode);
+ } else if (mode === NodeMode.replace) {
+ const newNode = h("placeholder", `note:${linkKey}`);
+ replace(node, newNode);
+ } else if (mode === NodeMode.direct || mode === NodeMode.default) {
+ const newChild = h("a", node.properties, node.children) as any;
+ newChild.properties.zhref = node.properties.href;
+ newChild.properties.href = link;
+ newChild.properties.ztype = "znotelink";
+ newChild.properties.class = "internal-link"; // required for obsidian compatibility
+ const newNode = h("znotelink", [newChild]);
+ replace(node, newNode);
+ }
+ }
+}
+
+async function processN2MRehypeImageNodes(
+ nodes: string | any[],
+ libraryID: number,
+ dir: string,
+ skipSavingImages: boolean = false,
+ absolutePath: boolean = false,
+ mode: NodeMode = NodeMode.default
+) {
+ if (!nodes.length) {
+ return;
+ }
+ for (const node of nodes) {
+ let imgKey = node.properties.dataAttachmentKey;
+
+ const attachmentItem = (await Zotero.Items.getByLibraryAndKeyAsync(
+ libraryID,
+ imgKey
+ )) as Zotero.Item;
+ if (!attachmentItem) {
+ continue;
+ }
+
+ let oldFile = String(await attachmentItem.getFilePathAsync());
+ let ext = oldFile.split(".").pop();
+ let newAbsPath = formatPath(`${dir}/${imgKey}.${ext}`);
+ let newFile = oldFile;
+ try {
+ // Don't overwrite
+ if (skipSavingImages || (await OS.File.exists(newAbsPath))) {
+ newFile = newAbsPath.replace(/\\/g, "/");
+ } else {
+ newFile = (await Zotero.File.copyToUnique(oldFile, newAbsPath)).path;
+ newFile = newFile.replace(/\\/g, "/");
+ }
+ newFile = Zotero.File.normalizeToUnix(
+ absolutePath ? newFile : `attachments/${newFile.split(/\//).pop()}`
+ );
+ } catch (e) {
+ ztoolkit.log(e);
+ }
+
+ node.properties.src = newFile ? newFile : oldFile;
+
+ if (mode === NodeMode.direct) {
+ const newChild = h("span") as any;
+ replace(newChild, node);
+ newChild.properties.ztype = "zimage";
+ // const newNode = h("zimage", [newChild]);
+ // replace(node, newNode);
+ node.properties.alt = toHtml(newChild);
+ }
+ }
+}
+
+function getM2NRehypeAnnotationNodes(rehype: any) {
+ const nodes: any[] = [];
+ visit(
+ rehype,
+ (node: any) => node.type === "element" && node.properties?.dataAnnotation,
+ (node: any) => nodes.push(node)
+ );
+ return new Array(...new Set(nodes));
+}
+
+function getM2NRehypeHighlightNodes(rehype: any) {
+ const nodes: any[] = [];
+ visit(
+ rehype,
+ (node: any) =>
+ node.type === "element" && node.properties?.ztype === "zhighlight",
+ (node) => nodes.push(node)
+ );
+ return new Array(...new Set(nodes));
+}
+
+function getM2NRehypeCitationNodes(rehype: any) {
+ const nodes: any[] = [];
+ visit(
+ rehype,
+ (node: any) =>
+ node.type === "element" &&
+ (node.properties?.ztype === "zcitation" || node.properties?.dataCitation),
+ (node) => nodes.push(node)
+ );
+ return new Array(...new Set(nodes));
+}
+
+function getM2NRehypeNoteLinkNodes(rehype: any) {
+ const nodes: any[] = [];
+ visit(
+ rehype,
+ (node: any) =>
+ node.type === "element" && node.properties?.ztype === "znotelink",
+ (node) => nodes.push(node)
+ );
+ return new Array(...new Set(nodes));
+}
+
+function getM2NRehypeImageNodes(rehype: any) {
+ const nodes: any[] = [];
+ visit(
+ rehype,
+ (node: any) => node.type === "element" && node.tagName === "img",
+ (node) => nodes.push(node)
+ );
+ return new Array(...new Set(nodes));
+}
+
+function processM2NRehypeMetaImageNodes(nodes: string | any[]) {
+ if (!nodes.length) {
+ return;
+ }
+
+ for (const node of nodes) {
+ if (/zimage/.test(node.properties.alt)) {
+ const newNode: any = unified()
+ .use(remarkGfm)
+ .use(remarkMath)
+ .use(rehypeParse, { fragment: true })
+ .parse(node.properties.alt);
+ newNode.properties.src = node.properties.src;
+ replace(node, newNode);
+ }
+ }
+}
+
+function processM2NRehypeHighlightNodes(nodes: string | any[]) {
+ if (!nodes.length) {
+ return;
+ }
+ for (const node of nodes) {
+ // node.children[0] is , its children is the real children
+ node.children = node.children[0].children;
+ delete node.properties.ztype;
+ }
+}
+
+async function processM2NRehypeCitationNodes(
+ nodes: string | any[],
+ isImport: boolean = false
+) {
+ if (!nodes.length) {
+ return;
+ }
+ for (const node of nodes) {
+ if (isImport) {
+ try {
+ // {
+ // "citationItems": [
+ // {
+ // "uris": [
+ // "http://zotero.org/users/uid/items/itemkey"
+ // ]
+ // }
+ // ],
+ // "properties": {}
+ // }
+ const dataCitation = JSON.parse(
+ decodeURIComponent(node.properties.dataCitation)
+ );
+ const ids = dataCitation.citationItems.map((c: { uris: string[] }) =>
+ Zotero.URI.getURIItemID(c.uris[0])
+ );
+ const html = (await parseCitationHTML(ids)) || "";
+ const newNode = note2rehype(html);
+ // root -> p -> span(cite, this is what we actually want)
+ replace(node, (newNode.children[0] as any).children[0]);
+ } catch (e) {
+ ztoolkit.log(e);
+ continue;
+ }
+ } else {
+ visit(
+ node,
+ (_n: any) => _n.properties?.className.includes("citation-item"),
+ (_n) => {
+ _n.children = [{ type: "text", value: toText(_n) }];
+ }
+ );
+ delete node.properties?.ztype;
+ }
+ }
+}
+
+function processM2NRehypeNoteLinkNodes(nodes: string | any[]) {
+ if (!nodes.length) {
+ return;
+ }
+ for (const node of nodes) {
+ node.properties.href = node.properties.zhref;
+ delete node.properties.class;
+ delete node.properties.zhref;
+ delete node.properties.ztype;
+ }
+}
+
+async function processM2NRehypeImageNodes(
+ this: any,
+ nodes: any[],
+ noteItem: Zotero.Item,
+ fileDir: string,
+ isImport: boolean = false
+) {
+ if (!nodes.length || (isImport && !noteItem)) {
+ return;
+ }
+
+ for (const node of nodes) {
+ if (isImport) {
+ // We encode the src in md2remark and decode it here.
+ let src = Zotero.File.normalizeToUnix(
+ decodeURIComponent(node.properties.src)
+ );
+ const srcType = (src as string).startsWith("data:")
+ ? "b64"
+ : (src as string).startsWith("http")
+ ? "url"
+ : "file";
+ if (srcType === "file") {
+ if (!(await OS.File.exists(src))) {
+ src = OS.Path.join(fileDir, src);
+ if (!(await OS.File.exists(src))) {
+ ztoolkit.log("parse image, path invalid");
+ continue;
+ }
+ }
+ }
+ const key = await importImageToNote(noteItem, src, srcType);
+ node.properties.dataAttachmentKey = key;
+ }
+ delete node.properties.src;
+ node.properties.ztype && delete node.properties.ztype;
+ }
+}
+
+enum NodeMode {
+ default = 0,
+ wrap,
+ replace,
+ direct,
+}
diff --git a/src/modules/editor/image.ts b/src/modules/editor/image.ts
new file mode 100644
index 0000000..2836fe1
--- /dev/null
+++ b/src/modules/editor/image.ts
@@ -0,0 +1,28 @@
+import { getPref } from "../../utils/prefs";
+
+export function initEditorImagePreviewer(editor: Zotero.EditorInstance) {
+ const openPreview = (e: MouseEvent) => {
+ const imgs = editor._iframeWindow.document
+ .querySelector(".primary-editor")
+ ?.querySelectorAll("img");
+ if (!imgs) {
+ return;
+ }
+ const imageList = Array.from(imgs);
+ addon.api.window.showImageViewer(
+ imageList.map((elem) => elem.src),
+ imageList.indexOf(e.target as HTMLImageElement),
+ editor._item.getNoteTitle()
+ );
+ };
+ editor._iframeWindow.document.addEventListener("dblclick", (e) => {
+ if ((e.target as HTMLElement).tagName === "IMG") {
+ openPreview(e);
+ }
+ });
+ editor._iframeWindow.document.addEventListener("click", (e) => {
+ if ((e.target as HTMLElement).tagName === "IMG" && e.ctrlKey) {
+ openPreview(e);
+ }
+ });
+}
diff --git a/src/modules/editor/initalize.ts b/src/modules/editor/initalize.ts
new file mode 100644
index 0000000..ea72f42
--- /dev/null
+++ b/src/modules/editor/initalize.ts
@@ -0,0 +1,34 @@
+import { initEditorImagePreviewer } from "./image";
+import { injectEditorCSS, injectEditorScripts } from "./inject";
+import { initEditorPopup } from "./popup";
+import { initEditorToolbar } from "./toolbar";
+
+export function registerEditorInstanceHook() {
+ Zotero.Notes.registerEditorInstance = new Proxy(
+ Zotero.Notes.registerEditorInstance,
+ {
+ apply: (
+ target,
+ thisArg,
+ argumentsList: [instance: Zotero.EditorInstance]
+ ) => {
+ target.apply(thisArg, argumentsList);
+ argumentsList.forEach(onEditorInstanceCreated);
+ },
+ }
+ );
+}
+
+async function onEditorInstanceCreated(editor: Zotero.EditorInstance) {
+ await editor._initPromise;
+
+ // item.getNote may not be initialized yet
+ if (Zotero.ItemTypes.getID("note") !== editor._item.itemTypeID) {
+ return;
+ }
+ await injectEditorScripts(editor._iframeWindow);
+ injectEditorCSS(editor._iframeWindow);
+ initEditorImagePreviewer(editor);
+ await initEditorToolbar(editor);
+ initEditorPopup(editor);
+}
diff --git a/src/modules/editor/inject.ts b/src/modules/editor/inject.ts
new file mode 100644
index 0000000..36b30ae
--- /dev/null
+++ b/src/modules/editor/inject.ts
@@ -0,0 +1,65 @@
+import { getFileContent } from "../../utils/str";
+
+export async function injectEditorScripts(win: Window) {
+ ztoolkit.UI.appendElement(
+ {
+ tag: "script",
+ id: "betternotes-script",
+ properties: {
+ innerHTML: await getFileContent(
+ rootURI + "chrome/content/scripts/editorScript.js"
+ ),
+ },
+ ignoreIfExists: true,
+ },
+ win.document.head
+ );
+}
+
+export function injectEditorCSS(win: Window) {
+ ztoolkit.UI.appendElement(
+ {
+ tag: "style",
+ id: "betternotes-style",
+ properties: {
+ innerHTML: `
+ .primary-editor > h1::before {
+ margin-left: -64px !important;
+ padding-left: 40px !important;
+ content: url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20width%3D%2218px%22%20height%3D%2218px%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2015.56%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%23666%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Ctitle%3E%E6%9C%AA%E6%A0%87%E9%A2%98-1%3C%2Ftitle%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M12.29%2C16.8H11.14V12.33H6.07V16.8H4.92V7H6.07v4.3h5.07V7h1.15Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M18.05%2C16.8H16.93V8.41a4%2C4%2C0%2C0%2C1-.9.53%2C6.52%2C6.52%2C0%2C0%2C1-1.14.44l-.32-1a8.2%2C8.2%2C0%2C0%2C0%2C1.67-.67%2C6.31%2C6.31%2C0%2C0%2C0%2C1.39-1h.42Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M21%2C5a2.25%2C2.25%2C0%2C0%2C1%2C2.25%2C2.25v9.56A2.25%2C2.25%2C0%2C0%2C1%2C21%2C19H3A2.25%2C2.25%2C0%2C0%2C1%2C.75%2C16.78V7.22A2.25%2C2.25%2C0%2C0%2C1%2C3%2C5H21m0-.75H3a3%2C3%2C0%2C0%2C0-3%2C3v9.56a3%2C3%2C0%2C0%2C0%2C3%2C3H21a3%2C3%2C0%2C0%2C0%2C3-3V7.22a3%2C3%2C0%2C0%2C0-3-3Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3C%2Fsvg%3E") !important;
+ }
+ .primary-editor > h2::before {
+ margin-left: -64px !important;
+ padding-left: 40px !important;
+ content: url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20width%3D%2218px%22%20height%3D%2218px%22%20%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2015.56%22%3E%3Cdefs%3E%3Cstyle%3E.a%7Bfill%3A%23666%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cpath%20class%3D%22a%22%20d%3D%22M11.17%2C16.8H10V12.33H5V16.8H3.8V7H5v4.3H10V7h1.15Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3Cpath%20class%3D%22a%22%20d%3D%22M14.14%2C16.8v-.48a4.1%2C4.1%2C0%2C0%2C1%2C.14-1.11%2C2.86%2C2.86%2C0%2C0%2C1%2C.45-.91%2C5.49%2C5.49%2C0%2C0%2C1%2C.83-.86c.33-.29.75-.61%2C1.24-1a7.43%2C7.43%2C0%2C0%2C0%2C.9-.73%2C3.9%2C3.9%2C0%2C0%2C0%2C.57-.7%2C2.22%2C2.22%2C0%2C0%2C0%2C.3-.66%2C2.87%2C2.87%2C0%2C0%2C0%2C.11-.77%2C1.89%2C1.89%2C0%2C0%2C0-.47-1.32%2C1.66%2C1.66%2C0%2C0%2C0-1.28-.5A3.17%2C3.17%2C0%2C0%2C0%2C15.7%2C8a3.49%2C3.49%2C0%2C0%2C0-1.08.76l-.68-.65a4.26%2C4.26%2C0%2C0%2C1%2C1.39-1A4%2C4%2C0%2C0%2C1%2C17%2C6.84a2.62%2C2.62%2C0%2C0%2C1%2C2.83%2C2.67%2C3.58%2C3.58%2C0%2C0%2C1-.15%2C1%2C3.09%2C3.09%2C0%2C0%2C1-.41.9%2C5.53%2C5.53%2C0%2C0%2C1-.67.81%2C9%2C9%2C0%2C0%2C1-.95.79c-.46.32-.84.59-1.13.82a4.68%2C4.68%2C0%2C0%2C0-.71.64%2C2%2C2%2C0%2C0%2C0-.38.6%2C2.08%2C2.08%2C0%2C0%2C0-.11.69h4.88v1Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3Cpath%20class%3D%22a%22%20d%3D%22M21%2C5a2.25%2C2.25%2C0%2C0%2C1%2C2.25%2C2.25v9.56A2.25%2C2.25%2C0%2C0%2C1%2C21%2C19H3A2.25%2C2.25%2C0%2C0%2C1%2C.75%2C16.78V7.22A2.25%2C2.25%2C0%2C0%2C1%2C3%2C5H21m0-.75H3a3%2C3%2C0%2C0%2C0-3%2C3v9.56a3%2C3%2C0%2C0%2C0%2C3%2C3H21a3%2C3%2C0%2C0%2C0%2C3-3V7.22a3%2C3%2C0%2C0%2C0-3-3Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3C%2Fsvg%3E") !important;
+ }
+ .primary-editor > h3::before {
+ margin-left: -64px !important;
+ padding-left: 40px !important;
+ content: url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20width%3D%2218px%22%20height%3D%2218px%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2015.56%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%23666%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M11.17%2C16.8H10V12.33H5V16.8H3.8V7H5v4.3H10V7h1.15Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M14%2C16.14l.51-.8a4.75%2C4.75%2C0%2C0%2C0%2C1.1.52%2C4.27%2C4.27%2C0%2C0%2C0%2C1.12.16%2C2.29%2C2.29%2C0%2C0%2C0%2C1.64-.52A1.77%2C1.77%2C0%2C0%2C0%2C19%2C14.17a1.7%2C1.7%2C0%2C0%2C0-.68-1.48%2C3.6%2C3.6%2C0%2C0%2C0-2.06-.48H15.4v-1h.77A3%2C3%2C0%2C0%2C0%2C18%2C10.81a1.65%2C1.65%2C0%2C0%2C0%2C.6-1.41%2C1.47%2C1.47%2C0%2C0%2C0-.47-1.19A1.67%2C1.67%2C0%2C0%2C0%2C17%2C7.79a3.33%2C3.33%2C0%2C0%2C0-2.08.73l-.59-.75a4.4%2C4.4%2C0%2C0%2C1%2C1.28-.71A4.35%2C4.35%2C0%2C0%2C1%2C17%2C6.84a2.84%2C2.84%2C0%2C0%2C1%2C2%2C.65%2C2.21%2C2.21%2C0%2C0%2C1%2C.74%2C1.78%2C2.35%2C2.35%2C0%2C0%2C1-.49%2C1.5%2C2.7%2C2.7%2C0%2C0%2C1-1.46.89v0a2.74%2C2.74%2C0%2C0%2C1%2C1.65.74%2C2.15%2C2.15%2C0%2C0%2C1%2C.66%2C1.65%2C2.64%2C2.64%2C0%2C0%2C1-.9%2C2.12%2C3.44%2C3.44%2C0%2C0%2C1-2.34.78%2C5.3%2C5.3%2C0%2C0%2C1-1.48-.2A5%2C5%2C0%2C0%2C1%2C14%2C16.14Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M21%2C5a2.25%2C2.25%2C0%2C0%2C1%2C2.25%2C2.25v9.56A2.25%2C2.25%2C0%2C0%2C1%2C21%2C19H3A2.25%2C2.25%2C0%2C0%2C1%2C.75%2C16.78V7.22A2.25%2C2.25%2C0%2C0%2C1%2C3%2C5H21m0-.75H3a3%2C3%2C0%2C0%2C0-3%2C3v9.56a3%2C3%2C0%2C0%2C0%2C3%2C3H21a3%2C3%2C0%2C0%2C0%2C3-3V7.22a3%2C3%2C0%2C0%2C0-3-3Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3C%2Fsvg%3E") !important;
+ }
+ .primary-editor > h4::before {
+ margin-left: -64px !important;
+ padding-left: 40px !important;
+ content: url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20width%3D%2218px%22%20height%3D%2218px%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2015.56%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%23666%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M11.17%2C16.8H10V12.33H5V16.8H3.8V7H5v4.3H10V7h1.15Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M19.43%2C6.92v6.59h1.05v1.05H19.43V16.9H18.31V14.56H13.66v-1c.43-.49.87-1%2C1.31-1.57s.87-1.13%2C1.27-1.7S17%2C9.14%2C17.36%2C8.57a16.51%2C16.51%2C0%2C0%2C0%2C.86-1.65Zm-4.49%2C6.59h3.37V8.63c-.34.61-.67%2C1.15-1%2C1.63s-.6.91-.87%2C1.3-.56.74-.81%2C1Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M21%2C5a2.25%2C2.25%2C0%2C0%2C1%2C2.25%2C2.25v9.56A2.25%2C2.25%2C0%2C0%2C1%2C21%2C19H3A2.25%2C2.25%2C0%2C0%2C1%2C.75%2C16.78V7.22A2.25%2C2.25%2C0%2C0%2C1%2C3%2C5H21m0-.75H3a3%2C3%2C0%2C0%2C0-3%2C3v9.56a3%2C3%2C0%2C0%2C0%2C3%2C3H21a3%2C3%2C0%2C0%2C0%2C3-3V7.22a3%2C3%2C0%2C0%2C0-3-3Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3C%2Fsvg%3E") !important;
+ }
+ .primary-editor > h5::before {
+ margin-left: -64px !important;
+ padding-left: 40px !important;
+ content: url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20width%3D%2218px%22%20height%3D%2218px%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2015.56%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%23666%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M11.17%2C16.8H10V12.33H5V16.8H3.8V7H5v4.3H10V7h1.15Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M14%2C16l.58-.76a3.67%2C3.67%2C0%2C0%2C0%2C1%2C.58A3.44%2C3.44%2C0%2C0%2C0%2C16.8%2C16a2.17%2C2.17%2C0%2C0%2C0%2C1.58-.6A2%2C2%2C0%2C0%2C0%2C19%2C13.88a1.85%2C1.85%2C0%2C0%2C0-.64-1.5%2C2.83%2C2.83%2C0%2C0%2C0-1.86-.54c-.27%2C0-.55%2C0-.86%2C0s-.58%2C0-.81.06L15.17%2C7H19.7V8H16.14l-.2%2C2.88.47%2C0h.43a3.5%2C3.5%2C0%2C0%2C1%2C2.43.79%2C2.74%2C2.74%2C0%2C0%2C1%2C.88%2C2.16%2C3%2C3%2C0%2C0%2C1-.94%2C2.3%2C3.41%2C3.41%2C0%2C0%2C1-2.4.87%2C4.45%2C4.45%2C0%2C0%2C1-1.5-.24A4.81%2C4.81%2C0%2C0%2C1%2C14%2C16Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M21%2C5a2.25%2C2.25%2C0%2C0%2C1%2C2.25%2C2.25v9.56A2.25%2C2.25%2C0%2C0%2C1%2C21%2C19H3A2.25%2C2.25%2C0%2C0%2C1%2C.75%2C16.78V7.22A2.25%2C2.25%2C0%2C0%2C1%2C3%2C5H21m0-.75H3a3%2C3%2C0%2C0%2C0-3%2C3v9.56a3%2C3%2C0%2C0%2C0%2C3%2C3H21a3%2C3%2C0%2C0%2C0%2C3-3V7.22a3%2C3%2C0%2C0%2C0-3-3Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3C%2Fsvg%3E") !important;
+ }
+ .primary-editor > h6::before {
+ margin-left: -64px !important;
+ padding-left: 40px !important;
+ content: url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20width%3D%2218px%22%20height%3D%2218px%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2015.56%22%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill%3A%23666%3B%7D%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M11.17%2C16.8H10V12.33H5V16.8H3.8V7H5v4.3H10V7h1.15Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M20.18%2C13.7a3.24%2C3.24%2C0%2C0%2C1-.88%2C2.38%2C2.94%2C2.94%2C0%2C0%2C1-2.2.9%2C2.69%2C2.69%2C0%2C0%2C1-2.31-1.17A5.59%2C5.59%2C0%2C0%2C1%2C14%2C12.49a12.18%2C12.18%2C0%2C0%2C1%2C.2-2.14%2C5.16%2C5.16%2C0%2C0%2C1%2C.84-2A3.65%2C3.65%2C0%2C0%2C1%2C16.27%2C7.2%2C3.71%2C3.71%2C0%2C0%2C1%2C18%2C6.84%2C3.14%2C3.14%2C0%2C0%2C1%2C19%2C7a3.59%2C3.59%2C0%2C0%2C1%2C1%2C.5l-.56.77a2.3%2C2.3%2C0%2C0%2C0-1.49-.48A2.3%2C2.3%2C0%2C0%2C0%2C16.79%2C8a3%2C3%2C0%2C0%2C0-.92.85%2C3.79%2C3.79%2C0%2C0%2C0-.56%2C1.25%2C6.56%2C6.56%2C0%2C0%2C0-.19%2C1.65h0a2.61%2C2.61%2C0%2C0%2C1%2C1-.84%2C2.91%2C2.91%2C0%2C0%2C1%2C1.23-.28%2C2.63%2C2.63%2C0%2C0%2C1%2C2%2C.85A3.09%2C3.09%2C0%2C0%2C1%2C20.18%2C13.7ZM19%2C13.78a2.28%2C2.28%2C0%2C0%2C0-.5-1.62%2C1.67%2C1.67%2C0%2C0%2C0-1.29-.54%2C2%2C2%2C0%2C0%2C0-1.5.58%2C2%2C2%2C0%2C0%2C0-.56%2C1.4%2C2.65%2C2.65%2C0%2C0%2C0%2C.55%2C1.74%2C1.85%2C1.85%2C0%2C0%2C0%2C2.78.1A2.38%2C2.38%2C0%2C0%2C0%2C19%2C13.78Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3Cpath%20class%3D%22cls-1%22%20d%3D%22M21%2C5a2.25%2C2.25%2C0%2C0%2C1%2C2.25%2C2.25v9.56A2.25%2C2.25%2C0%2C0%2C1%2C21%2C19H3A2.25%2C2.25%2C0%2C0%2C1%2C.75%2C16.78V7.22A2.25%2C2.25%2C0%2C0%2C1%2C3%2C5H21m0-.75H3a3%2C3%2C0%2C0%2C0-3%2C3v9.56a3%2C3%2C0%2C0%2C0%2C3%2C3H21a3%2C3%2C0%2C0%2C0%2C3-3V7.22a3%2C3%2C0%2C0%2C0-3-3Z%22%20transform%3D%22translate(0%20-4.22)%22%2F%3E%3C%2Fsvg%3E") !important;
+ }
+ .primary-editor > p, .primary-editor h1, .primary-editor h2, .primary-editor h3, .primary-editor h4, .primary-editor h5, .primary-editor h6, .primary-editor pre, .primary-editor blockquote, .primary-editor table, .primary-editor ul, .primary-editor ol, .primary-editor hr{
+ max-width: unset
+ }
+ `,
+ },
+ ignoreIfExists: true,
+ },
+ win.document.head
+ );
+}
diff --git a/src/modules/editor/popup.ts b/src/modules/editor/popup.ts
new file mode 100644
index 0000000..8e8d791
--- /dev/null
+++ b/src/modules/editor/popup.ts
@@ -0,0 +1,257 @@
+import { ICONS } from "../../utils/config";
+import {
+ del,
+ getEditorCore,
+ getLineAtCursor,
+ getPositionAtLine,
+ getURLAtCursor,
+ insert,
+ updateImageDimensionsAtCursor,
+ updateURLAtCursor,
+} from "../../utils/editor";
+import { getNoteLink, getNoteLinkParams } from "../../utils/link";
+import { getString } from "../../utils/locale";
+
+export function initEditorPopup(editor: Zotero.EditorInstance) {
+ const ob = new (ztoolkit.getGlobal("MutationObserver"))((muts) => {
+ for (const mut of muts) {
+ ztoolkit.log(mut);
+ if (
+ (mut.addedNodes.length &&
+ (mut.addedNodes[0] as HTMLElement).querySelector(".link-popup")) ||
+ (mut.attributeName === "href" &&
+ mut.target.parentElement?.classList.contains("link"))
+ ) {
+ updateEditorLinkPopup(editor);
+ } else if (
+ mut.addedNodes.length &&
+ (mut.addedNodes[0] as HTMLElement).querySelector(".image-popup")
+ ) {
+ updateEditorImagePopup(editor);
+ }
+ }
+ });
+ ob.observe(
+ editor._iframeWindow.document.querySelector(".relative-container")!,
+ {
+ subtree: true,
+ childList: true,
+ attributes: true,
+ attributeFilter: ["href"],
+ }
+ );
+}
+
+async function updateEditorLinkPopup(editor: Zotero.EditorInstance) {
+ const _window = editor._iframeWindow;
+ const link = getURLAtCursor(editor);
+ const linkParams = getNoteLinkParams(link);
+ const linkNote = linkParams.noteItem;
+ const editorNote = editor._item;
+ // If the note is invalid, we remove the buttons
+ if (linkNote) {
+ const insertButton = ztoolkit.UI.createElement(_window.document, "button", {
+ id: "link-popup-insert",
+ properties: {
+ title: `Import Linked Note: ${linkNote.getNoteTitle()}`,
+ innerHTML: ICONS["embedLinkContent"],
+ },
+ classList: ["link-popup-extra"],
+ removeIfExists: true,
+ listeners: [
+ {
+ type: "click",
+ listener: async (e) => {
+ if (!linkParams.ignore) {
+ const templateText = await addon.api.template.runTemplate(
+ "[QuickImportV2]",
+ "link, noteItem",
+ [link, editorNote]
+ );
+ // auto insert to anchor position
+ updateURLAtCursor(
+ editor,
+ undefined,
+ getNoteLink(
+ linkNote,
+ Object.assign({}, linkParams, { ignore: true })
+ )!
+ );
+ insert(editor, templateText);
+ } else {
+ updateURLAtCursor(
+ editor,
+ undefined,
+ getNoteLink(
+ linkNote,
+ Object.assign({}, linkParams, { ignore: null })
+ )!
+ );
+ const lineIndex = getLineAtCursor(editor);
+ del(
+ editor,
+ getPositionAtLine(editor, lineIndex),
+ getPositionAtLine(editor, lineIndex + 1)
+ );
+ }
+ },
+ },
+ ],
+ });
+
+ const updateButton = ztoolkit.UI.createElement(_window.document, "button", {
+ id: "link-popup-update",
+ properties: {
+ title: `Update Link Text: ${linkNote.getNoteTitle()}`,
+ innerHTML: ICONS["updateLinkText"],
+ },
+ classList: ["link-popup-extra"],
+ removeIfExists: true,
+ listeners: [
+ {
+ type: "click",
+ listener: async (e) => {
+ updateURLAtCursor(
+ editor,
+ linkNote.getNoteTitle(),
+ getURLAtCursor(editor)
+ );
+ },
+ },
+ ],
+ });
+
+ const openButton = ztoolkit.UI.createElement(_window.document, "button", {
+ id: "link-popup-open",
+ properties: {
+ title: "Open in new window",
+ innerHTML: ICONS["openInNewWindow"],
+ },
+ classList: ["link-popup-extra"],
+ removeIfExists: true,
+ listeners: [
+ {
+ type: "click",
+ listener: async (e) => {
+ ZoteroPane.openNoteWindow(linkNote.id);
+ },
+ },
+ ],
+ });
+
+ _window.document
+ .querySelector(".link-popup")
+ ?.append(insertButton, updateButton, openButton);
+ // if (linkPopup) {
+ // if (Zotero.Prefs.get("Knowledge4Zotero.linkAction.preview") as boolean) {
+ // let previewContainer =
+ // _window.document.getElementById("note-link-preview");
+ // if (previewContainer) {
+ // previewContainer.remove();
+ // }
+ // previewContainer = ztoolkit.UI.createElement(
+ // _window.document,
+ // "div"
+ // ) as HTMLDivElement;
+ // previewContainer.id = "note-link-preview";
+ // previewContainer.className = "ProseMirror primary-editor";
+ // previewContainer.innerHTML =
+ // await this._Addon.NoteParse.parseNoteStyleHTML(linkNote);
+ // previewContainer.addEventListener("click", (e) => {
+ // this._Addon.WorkspaceWindow.setWorkspaceNote("preview", linkNote);
+ // });
+ // linkPopup.append(previewContainer);
+ // previewContainer.setAttribute(
+ // "style",
+ // `width: 98%;height: ${
+ // linkPopup ? Math.min(linkPopup.offsetTop, 300) : 300
+ // }px;position: absolute;background: white;bottom: 36px;overflow: hidden;box-shadow: 0 0 5px 5px rgba(0,0,0,0.2);border-radius: 5px;cursor: pointer;opacity: 0.9;`
+ // );
+ // previewContainer
+ // .querySelector("div[data-schema-version]")
+ // ?.childNodes.forEach((node) => {
+ // if ((node as Element).setAttribute) {
+ // (node as Element).setAttribute("style", "margin: 0");
+ // } else {
+ // node.remove();
+ // }
+ // });
+ // }
+ // }
+ } else {
+ Array.from(_window.document.querySelectorAll(".link-popup-extra")).forEach(
+ (elem) => elem.remove()
+ );
+ }
+}
+
+function updateEditorImagePopup(editor: Zotero.EditorInstance) {
+ ztoolkit.UI.appendElement(
+ {
+ tag: "fragment",
+ children: [
+ {
+ tag: "button",
+ id: "image-popup-preview",
+ properties: {
+ innerHTML: ICONS.previewImage,
+ title: getString("editor.previewImage.title"),
+ },
+ removeIfExists: true,
+ listeners: [
+ {
+ type: "click",
+ listener: (e) => {
+ const imgs = editor._iframeWindow.document
+ .querySelector(".primary-editor")
+ ?.querySelectorAll("img");
+ if (!imgs) {
+ return;
+ }
+ const imageList = Array.from(imgs);
+ addon.api.window.showImageViewer(
+ imageList.map((elem) => elem.src),
+ imageList.indexOf(
+ editor._iframeWindow.document
+ .querySelector(".primary-editor")
+ ?.querySelector(".selected")
+ ?.querySelector("img") as HTMLImageElement
+ ),
+ editor._item.getNoteTitle()
+ );
+ },
+ },
+ ],
+ },
+ {
+ tag: "button",
+ id: "image-popup-resize",
+ properties: {
+ innerHTML: ICONS.resizeImage,
+ title: getString("editor.resizeImage.title"),
+ },
+ removeIfExists: true,
+ listeners: [
+ {
+ type: "click",
+ listener: (e) => {
+ const newWidth = parseFloat(
+ editor._iframeWindow.prompt(
+ getString("editor.resizeImage.prompt"),
+ // @ts-ignore
+ getEditorCore(editor).view.state.selection.node?.attrs
+ ?.width
+ ) || ""
+ );
+ if (newWidth && newWidth > 10) {
+ updateImageDimensionsAtCursor(editor, newWidth);
+ }
+ },
+ },
+ ],
+ },
+ ],
+ },
+ editor._iframeWindow.document.querySelector(".image-popup")!
+ );
+}
diff --git a/src/modules/editor/toolbar.ts b/src/modules/editor/toolbar.ts
new file mode 100644
index 0000000..d59c002
--- /dev/null
+++ b/src/modules/editor/toolbar.ts
@@ -0,0 +1,405 @@
+import { config } from "../../../package.json";
+import { ICONS } from "../../utils/config";
+import { getLineAtCursor } from "../../utils/editor";
+import { showHint } from "../../utils/hint";
+import { getNoteLink, getNoteLinkParams } from "../../utils/link";
+import { getString } from "../../utils/locale";
+import {
+ addLineToNote,
+ getNoteTreeFlattened,
+ getNoteType,
+} from "../../utils/note";
+import { getPref } from "../../utils/prefs";
+import { slice } from "../../utils/str";
+
+export async function initEditorToolbar(editor: Zotero.EditorInstance) {
+ const noteItem = editor._item;
+ const noteType = getNoteType(noteItem.id);
+ const toolbar = await registerEditorToolbar(editor, makeId("toolbar"));
+
+ // Settings
+ const settingsButton = await registerEditorToolbarDropdown(
+ editor,
+ toolbar,
+ makeId("settings"),
+ ICONS.settings,
+ getString("editor.toolbar.settings.title"),
+ "start",
+ (e) => {}
+ );
+
+ settingsButton.addEventListener("mouseenter", (ev) => {
+ const settingsMenu: PopupData[] = [
+ {
+ id: makeId("settings-openWorkspace"),
+ text: getString("editor.toolbar.settings.openWorkspace"),
+ callback: (e) => {
+ addon.hooks.onOpenWorkspace("tab");
+ },
+ },
+ {
+ id: makeId("settings-setWorkspace"),
+ text: getString("editor.toolbar.settings.setWorkspace"),
+ callback: (e) => {
+ addon.hooks.onSetWorkspaceNote(e.editor._item.id, "main");
+ },
+ },
+ {
+ id: makeId("settings-insertTemplate"),
+ text: getString("editor.toolbar.settings.insertTemplate"),
+ callback: (e) => {
+ addon.api.window.showTemplatePicker(
+ e.editor._item.id,
+ getLineAtCursor(e.editor)
+ );
+ },
+ },
+ {
+ id: makeId("settings-copyLink"),
+ text: getString("editor.toolbar.settings.copyLink"),
+ callback: (e) => {
+ const link =
+ getNoteLink(e.editor._item, {
+ lineIndex: getLineAtCursor(e.editor),
+ }) || "";
+ new ztoolkit.Clipboard()
+ .addText(link, "text/unicode")
+ .addText(
+ ` ${
+ e.editor._item.getNoteTitle().trim() || link
+ } `,
+ "text/html"
+ )
+ .copy();
+ showHint(`Link ${link} copied`);
+ },
+ },
+ ];
+
+ const settingsPopup = registerEditorToolbarPopup(
+ editor,
+ settingsButton,
+ `${config.addonRef}-settings-popup`,
+ "left",
+ settingsMenu
+ );
+ });
+ settingsButton.addEventListener("mouseleave", (ev) => {
+ editor._iframeWindow.document
+ .querySelector(`#${makeId("settings-popup")}`)
+ ?.remove();
+ });
+ settingsButton.addEventListener("click", (ev) => {
+ editor._iframeWindow.document
+ .querySelector(`#${makeId("settings-popup")}`)
+ ?.remove();
+ });
+
+ // Center button
+ if (noteType === "main") {
+ registerEditorToolbarElement(
+ editor,
+ toolbar,
+ "middle",
+ ztoolkit.UI.createElement(editor._iframeWindow.document, "div", {
+ properties: { innerHTML: getString("editor.toolbar.main") },
+ })
+ );
+ } else {
+ const onTriggerMenu = (ev: MouseEvent) => {
+ editor._iframeWindow.focus();
+ const linkMenu: PopupData[] = getLinkMenuData(editor);
+ editor._iframeWindow.document
+ .querySelector(`#${makeId("link")}`)!
+ .querySelector(".toolbar-button")!.innerHTML = ICONS.linkAfter;
+
+ const popup = registerEditorToolbarPopup(
+ editor,
+ linkButton,
+ `${config.addonRef}-link-popup`,
+ "middle",
+ linkMenu
+ );
+ };
+
+ const onExitMenu = (ev: MouseEvent) => {
+ editor._iframeWindow.document
+ .querySelector(`#${makeId("link-popup")}`)
+ ?.remove();
+ editor._iframeWindow.document
+ .querySelector(`#${makeId("link")}`)!
+ .querySelector(".toolbar-button")!.innerHTML = ICONS.addon;
+ };
+
+ const onClickMenu = async (ev: MouseEvent) => {
+ const mainNote = Zotero.Items.get(addon.data.workspace.mainId) || null;
+ if (!mainNote?.isNote()) {
+ return;
+ }
+ const lineIndex = parseInt(
+ (ev.target as HTMLDivElement).id.split("-").pop() || "-1"
+ );
+ const forwardLink = getNoteLink(noteItem);
+ const backLink = getNoteLink(mainNote, { ignore: true, lineIndex });
+ addLineToNote(
+ mainNote,
+ await addon.api.template.runTemplate(
+ "[QuickInsertV2]",
+ "link, linkText, subNoteItem, noteItem",
+ [
+ forwardLink,
+ noteItem.getNoteTitle().trim() || forwardLink,
+ noteItem,
+ mainNote,
+ ]
+ ),
+ lineIndex
+ );
+ addLineToNote(
+ noteItem,
+ await addon.api.template.runTemplate(
+ "[QuickBackLinkV2]",
+ "link, linkText, subNoteItem, noteItem",
+ [
+ backLink,
+ mainNote.getNoteTitle().trim() || "Workspace Note",
+ noteItem,
+ mainNote,
+ "",
+ ]
+ )
+ );
+ onExitMenu(ev);
+ ev.stopPropagation();
+ };
+
+ const linkButton = await registerEditorToolbarDropdown(
+ editor,
+ toolbar,
+ makeId("link"),
+ ICONS.addon,
+ getString("editor.toolbar.link.title"),
+ "middle",
+ onClickMenu
+ );
+
+ linkButton.addEventListener("mouseenter", onTriggerMenu);
+ linkButton.addEventListener("mouseleave", onExitMenu);
+ linkButton.addEventListener("mouseleave", onExitMenu);
+ linkButton.addEventListener("click", (ev) => {
+ if ((ev.target as HTMLElement).classList.contains("option")) {
+ onClickMenu(ev);
+ }
+ });
+ }
+
+ // Export
+ const exportButton = await registerEditorToolbarDropdown(
+ editor,
+ toolbar,
+ makeId("export"),
+ ICONS.export,
+ getString("editor.toolbar.export.title"),
+ "end",
+ (e) => {
+ if (addon.api.sync.isSyncNote(noteItem.id)) {
+ addon.api.window.showSyncInfo(noteItem.id);
+ } else {
+ addon.api.window.showExportNoteOptions([noteItem.id]);
+ }
+ }
+ );
+}
+
+function getLinkMenuData(editor: Zotero.EditorInstance): PopupData[] {
+ const workspaceNote = Zotero.Items.get(addon.data.workspace.mainId) || null;
+ const currentNote = editor._item;
+ if (!workspaceNote?.isNote()) {
+ return [
+ {
+ id: makeId("link-popup-nodata"),
+ text: getString("editor.toolbar.link.popup.nodata"),
+ },
+ ];
+ }
+ const nodes = getNoteTreeFlattened(workspaceNote, {
+ keepLink: true,
+ });
+ const menuData: PopupData[] = [];
+ for (const node of nodes) {
+ if (node.model.level === 7) {
+ const lastMenu =
+ menuData.length > 0 ? menuData[menuData.length - 1] : null;
+ const linkNote = getNoteLinkParams(node.model.link).noteItem;
+ if (linkNote && linkNote.id === currentNote.id && lastMenu) {
+ lastMenu.suffix = "🔗";
+ }
+ continue;
+ }
+ menuData.push({
+ id: makeId(
+ `link-popup-${
+ getPref("editor.link.insertPosition")
+ ? node.model.lineIndex - 1
+ : node.model.endIndex
+ }`
+ ),
+ text: node.model.name,
+ prefix: "·".repeat(node.model.level - 1),
+ });
+ }
+ return menuData;
+}
+
+async function registerEditorToolbar(
+ editor: Zotero.EditorInstance,
+ id: string
+) {
+ await editor._initPromise;
+ const _document = editor._iframeWindow.document;
+ const toolbar = ztoolkit.UI.createElement(_document, "div", {
+ attributes: {
+ id,
+ },
+ classList: ["toolbar"],
+ children: [
+ {
+ tag: "div",
+ classList: ["start"],
+ },
+ {
+ tag: "div",
+ classList: ["middle"],
+ },
+ {
+ tag: "div",
+ classList: ["end"],
+ },
+ ],
+ ignoreIfExists: true,
+ }) as HTMLDivElement;
+ _document.querySelector(".editor")?.childNodes[0].before(toolbar);
+ return toolbar;
+}
+
+async function registerEditorToolbarDropdown(
+ editor: Zotero.EditorInstance,
+ toolbar: HTMLDivElement,
+ id: string,
+ icon: string,
+ title: string,
+ position: "start" | "middle" | "end",
+ callback: (e: MouseEvent & { editor: Zotero.EditorInstance }) => any
+) {
+ await editor._initPromise;
+ const _document = editor._iframeWindow.document;
+ const dropdown = ztoolkit.UI.createElement(_document, "div", {
+ attributes: {
+ id,
+ title,
+ },
+ classList: ["dropdown", "more-dropdown"],
+ children: [
+ {
+ tag: "button",
+ attributes: {
+ title,
+ },
+ properties: {
+ innerHTML: icon,
+ },
+ classList: ["toolbar-button"],
+ listeners: [
+ {
+ type: "click",
+ listener: (e) => {
+ Object.assign(e, { editor });
+ if (callback) {
+ callback(
+ e as any as MouseEvent & { editor: Zotero.EditorInstance }
+ );
+ }
+ },
+ },
+ ],
+ },
+ ],
+ skipIfExists: true,
+ });
+ toolbar.querySelector(`.${position}`)?.append(dropdown);
+ return dropdown;
+}
+
+declare interface PopupData {
+ id: string;
+ text: string;
+ prefix?: string;
+ suffix?: string;
+ callback?: (e: MouseEvent & { editor: Zotero.EditorInstance }) => any;
+}
+
+async function registerEditorToolbarPopup(
+ editor: Zotero.EditorInstance,
+ dropdown: HTMLDivElement,
+ id: string,
+ align: "middle" | "left" | "right",
+ popupLines: PopupData[]
+) {
+ await editor._initPromise;
+ const popup = ztoolkit.UI.appendElement(
+ {
+ tag: "div",
+ classList: ["popup"],
+ id,
+ children: popupLines.map((props) => ({
+ tag: "button",
+ classList: ["option"],
+ properties: {
+ id: props.id,
+ innerHTML:
+ slice((props.prefix || "") + props.text, 30) + (props.suffix || ""),
+ title: "",
+ },
+ listeners: [
+ {
+ type: "click",
+ listener: (e) => {
+ Object.assign(e, { editor });
+ props.callback &&
+ props.callback(
+ e as any as MouseEvent & { editor: Zotero.EditorInstance }
+ );
+ },
+ },
+ ],
+ })),
+ removeIfExists: true,
+ },
+ dropdown
+ ) as HTMLDivElement;
+ let style: string = "";
+ if (align === "middle") {
+ style = `right: -${popup.offsetWidth / 2 - 15}px;`;
+ } else if (align === "left") {
+ style = "left: 0; right: auto;";
+ } else if (align === "right") {
+ style = "right: 0;";
+ }
+ popup.setAttribute("style", style);
+ return popup;
+}
+
+async function registerEditorToolbarElement(
+ editor: Zotero.EditorInstance,
+ toolbar: HTMLDivElement,
+ position: "start" | "middle" | "end",
+ elem: HTMLElement
+) {
+ await editor._initPromise;
+ toolbar.querySelector(`.${position}`)?.append(elem);
+ return elem;
+}
+
+function makeId(key: string) {
+ return `${config.addonRef}-${key}`;
+}
diff --git a/src/modules/export/api.ts b/src/modules/export/api.ts
new file mode 100644
index 0000000..2d9a08a
--- /dev/null
+++ b/src/modules/export/api.ts
@@ -0,0 +1,218 @@
+import {
+ getLinkedNotesRecursively,
+ getNoteLink,
+ getNoteLinkParams,
+} from "../../utils/link";
+import { getString } from "../../utils/locale";
+import { getLinesInNote } from "../../utils/note";
+import { formatPath } from "../../utils/str";
+
+export { exportNotes };
+
+async function exportNotes(
+ noteItems: Zotero.Item[],
+ options: {
+ embedLink?: boolean;
+ standaloneLink?: boolean;
+ exportNote?: boolean;
+ exportMD?: boolean;
+ setAutoSync?: 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) {
+ let 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.api.sync.doSync(allNoteItems, {
+ quiet: true,
+ skipActive: false,
+ reason: "export",
+ });
+ }
+ } else {
+ for (const noteItem of allNoteItems) {
+ await toMD(noteItem, {
+ 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.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.exportPDF) {
+ for (const noteItem of allNoteItems) {
+ await addon.api._export.savePDF(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 {
+ const parser = ztoolkit.getDOMParser();
+
+ let newLines: string[] = [];
+ const noteLines = getLinesInNote(noteItem);
+ for (let 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);
+ }
+ }
+ return newLines.join("\n");
+}
diff --git a/src/modules/export/docx.ts b/src/modules/export/docx.ts
new file mode 100644
index 0000000..5cc0f0f
--- /dev/null
+++ b/src/modules/export/docx.ts
@@ -0,0 +1,68 @@
+import { showHintWithLink } from "../../utils/hint";
+import { renderNoteHTML } from "../../utils/note";
+import { getFileContent, randomString } from "../../utils/str";
+import { waitUtilAsync } from "../../utils/wait";
+
+export async function saveDocx(filename: string, noteId: number) {
+ const noteItem = Zotero.Items.get(noteId);
+ await Zotero.File.putContentsAsync(filename, await note2docx(noteItem));
+ showHintWithLink(`Note Saved to ${filename}`, "Show in Folder", (ev) => {
+ Zotero.File.reveal(filename);
+ });
+}
+
+async function note2docx(noteItem: Zotero.Item) {
+ const renderedContent = await renderNoteHTML(noteItem);
+ let htmlDoc =
+ '\n \n';
+ htmlDoc += renderedContent;
+ htmlDoc += "\n";
+
+ let blob: ArrayBufferLike;
+ const lock = Zotero.Promise.defer();
+ const jobId = randomString(6, new Date().toUTCString());
+ const listener = (ev: MessageEvent) => {
+ if (ev.data.type === "parseDocxReturn" && ev.data.jobId === jobId) {
+ blob = ev.data.message;
+ lock.resolve();
+ }
+ };
+ const worker = await getWorker();
+ worker.contentWindow?.addEventListener("message", listener);
+ worker.contentWindow?.postMessage(
+ {
+ type: "parseDocx",
+ jobId,
+ message: htmlDoc,
+ },
+ "*"
+ );
+ await lock.promise;
+ worker.contentWindow?.removeEventListener("message", listener);
+ return blob!;
+}
+
+async function getWorker() {
+ if (addon.data.export.docx.worker) {
+ return addon.data.export.docx.worker;
+ }
+ const worker = Zotero.Browser.createHiddenBrowser(
+ window
+ ) as HTMLIFrameElement;
+ await waitUtilAsync(() => worker.contentDocument?.readyState === "complete");
+
+ const doc = worker.contentDocument;
+ ztoolkit.UI.appendElement(
+ {
+ tag: "script",
+ properties: {
+ innerHTML: await getFileContent(
+ rootURI + "chrome/content/scripts/docxWorker.js"
+ ),
+ },
+ },
+ doc?.head!
+ );
+ addon.data.export.docx.worker = worker;
+ return worker;
+}
diff --git a/src/modules/export/exportWindow.ts b/src/modules/export/exportWindow.ts
new file mode 100644
index 0000000..c97266d
--- /dev/null
+++ b/src/modules/export/exportWindow.ts
@@ -0,0 +1,227 @@
+import { getString } from "../../utils/locale";
+import { getPref, setPref } from "../../utils/prefs";
+import { fill, slice } from "../../utils/str";
+
+enum OPTIONS {
+ "embedLink",
+ "standaloneLink",
+ "keepLink",
+ "exportMD",
+ "setAutoSync",
+ "withYAMLHeader",
+ "exportDocx",
+ "exportPDF",
+ "exportFreeMind",
+ "exportNote",
+}
+
+export async function showExportNoteOptions(noteIds: number[]) {
+ const items = Zotero.Items.get(noteIds);
+ const noteItems: Zotero.Item[] = [];
+ items.forEach((item) => {
+ if (item.isNote()) {
+ noteItems.push(item);
+ }
+ if (item.isRegularItem()) {
+ noteItems.splice(0, 0, ...Zotero.Items.get(item.getNotes()));
+ }
+ });
+ if (noteItems.length === 0) {
+ return;
+ }
+ const dataKeys = Object.keys(OPTIONS).filter(
+ (value) => typeof value === "string"
+ );
+ const data = dataKeys.reduce((acc, key) => {
+ acc[key] = getPref(`export.${key}`) as boolean;
+ return acc;
+ }, {} as Record);
+
+ data.loadCallback = () => {
+ const doc = dialog.window.document;
+ const standaloneLinkRadio = doc.querySelector(
+ "#standaloneLink"
+ ) as HTMLInputElement;
+ const autoSyncRadio = doc.querySelector("#setAutoSync") as HTMLInputElement;
+ function updateSyncCheckbox() {
+ const standaloneLinkEnabled = standaloneLinkRadio.checked;
+ if (!standaloneLinkEnabled) {
+ autoSyncRadio.checked = false;
+ autoSyncRadio.disabled = true;
+ } else {
+ autoSyncRadio.disabled = false;
+ }
+ }
+ Array.from(doc.querySelectorAll('input[name="linkMode"]')).forEach((elem) =>
+ elem.addEventListener("change", updateSyncCheckbox)
+ );
+ updateSyncCheckbox();
+ };
+
+ const dialog = new ztoolkit.Dialog(17, 1)
+ .setDialogData(data)
+ .addCell(0, 0, {
+ tag: "div",
+ styles: {
+ display: "grid",
+ gridTemplateColumns: "1fr 20px",
+ rowGap: "10px",
+ columnGap: "5px",
+ },
+ children: [
+ {
+ tag: "label",
+ properties: {
+ innerHTML: `${getString("export.target")}: ${fill(
+ slice(noteItems[0].getNoteTitle(), 40),
+ 40
+ )}${
+ noteItems.length > 1 ? ` and ${noteItems.length - 1} more` : ""
+ }`,
+ },
+ },
+ ],
+ })
+ .addCell(1, 0, makeHeadingLine(getString("export.options.linkMode")))
+ .addCell(2, 0, makeRadioLine("embedLink", "linkMode"))
+ .addCell(3, 0, makeRadioLine("standaloneLink", "linkMode"))
+ .addCell(4, 0, makeRadioLine("keepLink", "linkMode"))
+ .addCell(5, 0, makeHeadingLine(getString("export.options.MD")))
+ .addCell(6, 0, makeCheckboxLine("exportMD"))
+ .addCell(7, 0, makeCheckboxLine("setAutoSync"))
+ .addCell(8, 0, makeCheckboxLine("withYAMLHeader"))
+ .addCell(9, 0, makeHeadingLine(getString("export.options.Docx")))
+ .addCell(10, 0, makeCheckboxLine("exportDocx"))
+ .addCell(11, 0, makeHeadingLine(getString("export.options.PDF")))
+ .addCell(12, 0, makeCheckboxLine("exportPDF"))
+ .addCell(13, 0, makeHeadingLine(getString("export.options.mm")))
+ .addCell(14, 0, makeCheckboxLine("exportFreeMind"))
+ .addCell(15, 0, makeHeadingLine(getString("export.options.note")))
+ .addCell(16, 0, makeCheckboxLine("exportNote"))
+ .addButton(getString("export.confirm"), "confirm")
+ .addButton(getString("export.cancel"), "cancel")
+ .open(getString("export.title"), {
+ resizable: true,
+ centerscreen: true,
+ fitContent: true,
+ noDialogMode: true,
+ });
+
+ await data.unloadLock?.promise;
+ if (data._lastButtonId === "confirm") {
+ addon.api._export.exportNotes(noteItems, data as Record);
+ dataKeys.forEach((key) => {
+ setPref(`export.${key}`, Boolean(data[key]));
+ });
+ }
+}
+
+function makeHeadingLine(text: string) {
+ return {
+ tag: "div",
+ styles: {
+ display: "grid",
+ gridTemplateColumns: "1fr 20px",
+ rowGap: "10px",
+ columnGap: "5px",
+ },
+ children: [
+ {
+ tag: "h3",
+ properties: {
+ innerHTML: text,
+ },
+ },
+ ],
+ };
+}
+
+function makeCheckboxLine(dataKey: string, callback?: (ev: Event) => void) {
+ return {
+ tag: "div",
+ styles: {
+ display: "grid",
+ gridTemplateColumns: "1fr 20px",
+ rowGap: "10px",
+ columnGap: "5px",
+ },
+ children: [
+ {
+ tag: "label",
+ attributes: {
+ for: dataKey,
+ },
+ properties: {
+ innerHTML: getString(`export.${dataKey}`),
+ },
+ },
+ {
+ tag: "input",
+ id: dataKey,
+ attributes: {
+ "data-bind": dataKey,
+ "data-prop": "checked",
+ },
+ properties: {
+ type: "checkbox",
+ },
+ listeners: callback
+ ? [
+ {
+ type: "change",
+ listener: callback,
+ },
+ ]
+ : [],
+ },
+ ],
+ };
+}
+
+function makeRadioLine(
+ dataKey: string,
+ radioName: string,
+ callback?: (ev: Event) => void
+) {
+ return {
+ tag: "div",
+ styles: {
+ display: "grid",
+ gridTemplateColumns: "1fr 20px",
+ rowGap: "10px",
+ columnGap: "5px",
+ },
+ children: [
+ {
+ tag: "label",
+ attributes: {
+ for: dataKey,
+ },
+ properties: {
+ innerHTML: getString(`export.${dataKey}`),
+ },
+ },
+ {
+ tag: "input",
+ id: dataKey,
+ attributes: {
+ "data-bind": dataKey,
+ "data-prop": "checked",
+ },
+ properties: {
+ type: "radio",
+ name: radioName,
+ value: dataKey,
+ },
+ listeners: callback
+ ? [
+ {
+ type: "change",
+ listener: callback,
+ },
+ ]
+ : [],
+ },
+ ],
+ };
+}
diff --git a/src/modules/export/freemind.ts b/src/modules/export/freemind.ts
new file mode 100644
index 0000000..fd1018a
--- /dev/null
+++ b/src/modules/export/freemind.ts
@@ -0,0 +1,90 @@
+import TreeModel = require("tree-model");
+import { showHintWithLink } from "../../utils/hint";
+import { getNoteTree, parseHTMLLines, renderNoteHTML } from "../../utils/note";
+
+export async function saveFreeMind(filename: string, noteId: number) {
+ const noteItem = Zotero.Items.get(noteId);
+ await Zotero.File.putContentsAsync(filename, await note2mm(noteItem));
+ showHintWithLink(`Note Saved to ${filename}`, "Show in Folder", (ev) => {
+ Zotero.File.reveal(filename);
+ });
+}
+
+async function note2mm(
+ noteItem: Zotero.Item,
+ options: { withContent?: boolean } = { withContent: true }
+) {
+ const root = getNoteTree(noteItem, false);
+ const textNodeForEach = (e: Node, callbackfn: Function) => {
+ if (e.nodeType === document.TEXT_NODE) {
+ callbackfn(e);
+ return;
+ }
+ e.childNodes.forEach((_e) => textNodeForEach(_e, callbackfn));
+ };
+ const html2Escape = (sHtml: string) => {
+ return sHtml.replace(/[<>&"]/g, function (c) {
+ return { "<": "<", ">": ">", "&": "&", '"': """ }[c]!;
+ });
+ };
+ let lines: string[] = [];
+ if (options.withContent) {
+ const doc = ztoolkit
+ .getDOMParser()
+ .parseFromString(await renderNoteHTML(noteItem), "text/html");
+ textNodeForEach(doc.body, (e: Text) => {
+ e.data = html2Escape(e.data);
+ });
+ lines = parseHTMLLines(doc.body.innerHTML);
+ }
+ const convertClosingTags = (htmlStr: string) => {
+ const regConfs = [
+ {
+ reg: / ]*?>/g,
+ cbk: (str: string) => " ",
+ },
+ {
+ reg: / ]*?>/g,
+ cbk: (str: string) => {
+ return ` `;
+ },
+ },
+ ];
+ for (const regConf of regConfs) {
+ htmlStr = htmlStr.replace(regConf.reg, regConf.cbk);
+ }
+ return htmlStr;
+ };
+ const convertNode = (node: TreeModel.Node) => {
+ mmXML += ` `;
+ if (
+ options.withContent &&
+ node.model.lineIndex >= 0 &&
+ node.model.endIndex >= 0
+ ) {
+ mmXML += `${convertClosingTags(
+ lines
+ .slice(
+ node.model.lineIndex,
+ node.hasChildren()
+ ? node.children[0].model.lineIndex
+ : node.model.endIndex + 1
+ )
+ .join("\n")
+ )} `;
+ }
+ if (node.hasChildren()) {
+ node.children.forEach((child: TreeModel.Node) => {
+ convertNode(child);
+ });
+ }
+ mmXML += " ";
+ };
+ let mmXML = '';
+ convertNode(root);
+ mmXML += " ";
+ ztoolkit.log(mmXML);
+ return mmXML;
+}
diff --git a/src/modules/export/markdown.ts b/src/modules/export/markdown.ts
new file mode 100644
index 0000000..ed8fb67
--- /dev/null
+++ b/src/modules/export/markdown.ts
@@ -0,0 +1,60 @@
+import { showHintWithLink } from "../../utils/hint";
+import { formatPath } from "../../utils/str";
+
+export async function saveMD(
+ filename: string,
+ noteId: number,
+ options: {
+ keepNoteLink?: boolean;
+ withYAMLHeader?: boolean;
+ }
+) {
+ const noteItem = Zotero.Items.get(noteId);
+ const dir = OS.Path.join(
+ ...OS.Path.split(formatPath(filename)).components.slice(0, -1)
+ );
+ const hasImage = noteItem.getNote().includes(" {
+ Zotero.File.reveal(filename);
+ });
+}
+
+export async function syncMDBatch(saveDir: string, noteIds: number[]) {
+ const noteItems = Zotero.Items.get(noteIds);
+ await Zotero.File.createDirectoryIfMissingAsync(saveDir);
+ const attachmentsDir = formatPath(OS.Path.join(saveDir, "attachments"));
+ const hasImage = noteItems.some((noteItem) =>
+ noteItem.getNote().includes(" win.document.readyState === "complete");
+ win.document.querySelector(".markdown-body")!.innerHTML = html;
+ const printPromise = Zotero.Promise.defer();
+ disablePrintFooterHeader();
+ win.addEventListener("mouseover", (ev) => {
+ win.close();
+ printPromise.resolve();
+ });
+ win.print();
+ await printPromise.promise;
+ showHint("Note Saved as PDF");
+}
+
+function disablePrintFooterHeader() {
+ // @ts-ignore
+ Zotero.Prefs.resetBranch([], "print");
+ Zotero.Prefs.set("print.print_footercenter", "", true);
+ Zotero.Prefs.set("print.print_footerleft", "", true);
+ Zotero.Prefs.set("print.print_footerright", "", true);
+ Zotero.Prefs.set("print.print_headercenter", "", true);
+ Zotero.Prefs.set("print.print_headerleft", "", true);
+ Zotero.Prefs.set("print.print_headerright", "", true);
+}
diff --git a/src/modules/imageViewer.ts b/src/modules/imageViewer.ts
new file mode 100644
index 0000000..def5b37
--- /dev/null
+++ b/src/modules/imageViewer.ts
@@ -0,0 +1,293 @@
+import { config } from "../../package.json";
+import { ICONS } from "../utils/config";
+import { showHint, showHintWithLink } from "../utils/hint";
+import { formatPath } from "../utils/str";
+import { waitUtilAsync } from "../utils/wait";
+
+export async function showImageViewer(
+ srcList: string[],
+ idx: number,
+ title: string
+) {
+ if (
+ !addon.data.imageViewer.window ||
+ Components.utils.isDeadWrapper(addon.data.imageViewer.window) ||
+ addon.data.imageViewer.window.closed
+ ) {
+ addon.data.imageViewer.window = window.openDialog(
+ `chrome://${config.addonRef}/content/imageViewer.html`,
+ `${config.addonRef}-imageViewer`,
+ `chrome,centerscreen,resizable,status,width=500,height=550,dialog=no${
+ addon.data.imageViewer.pined ? ",alwaysRaised=yes" : ""
+ }`
+ )!;
+ await waitUtilAsync(
+ () => addon.data.imageViewer.window?.document.readyState === "complete"
+ );
+ const container = addon.data.imageViewer.window.document.querySelector(
+ ".container"
+ ) as HTMLDivElement;
+ const img = addon.data.imageViewer.window.document.querySelector(
+ "#image"
+ ) as HTMLImageElement;
+
+ addon.data.imageViewer.window.document
+ .querySelector("#left")
+ ?.addEventListener("click", (e) => {
+ setIndex("left");
+ });
+ addon.data.imageViewer.window.document
+ .querySelector("#bigger")
+ ?.addEventListener("click", (e) => {
+ addon.data.imageViewer.anchorPosition = {
+ left: img.scrollWidth / 2 - container.scrollLeft / 2,
+ top: img.scrollHeight / 2 - container.scrollLeft / 2,
+ };
+ setScale(addon.data.imageViewer.scaling * 1.1);
+ });
+ addon.data.imageViewer.window.document
+ .querySelector("#smaller")
+ ?.addEventListener("click", (e) => {
+ addon.data.imageViewer.anchorPosition = {
+ left: img.scrollWidth / 2 - container.scrollLeft / 2,
+ top: img.scrollHeight / 2 - container.scrollLeft / 2,
+ };
+ setScale(addon.data.imageViewer.scaling / 1.1);
+ });
+ addon.data.imageViewer.window.document
+ .querySelector("#resetwidth")
+ ?.addEventListener("click", (e) => {
+ setScale(1);
+ });
+ addon.data.imageViewer.window.document
+ .querySelector("#right")
+ ?.addEventListener("click", (e) => {
+ setIndex("right");
+ });
+ addon.data.imageViewer.window.document
+ .querySelector("#copy")
+ ?.addEventListener("click", (e) => {
+ new ztoolkit.Clipboard()
+ .addImage(addon.data.imageViewer.srcList[addon.data.imageViewer.idx])
+ .copy();
+ showHint("Image Copied.");
+ });
+ addon.data.imageViewer.window.document
+ .querySelector("#save")
+ ?.addEventListener("click", async (e) => {
+ let parts =
+ addon.data.imageViewer.srcList[addon.data.imageViewer.idx].split(",");
+ if (!parts[0].includes("base64")) {
+ return;
+ }
+ let mime = parts[0].match(/:(.*?);/)![1];
+ let bstr = addon.data.imageViewer.window?.atob(parts[1])!;
+ let n = bstr.length;
+ let u8arr = new Uint8Array(n);
+ while (n--) {
+ u8arr[n] = bstr.charCodeAt(n);
+ }
+ let ext = Zotero.MIME.getPrimaryExtension(mime, "");
+ const filename = await new ztoolkit.FilePicker(
+ Zotero.getString("noteEditor.saveImageAs"),
+ "save",
+ [[`Image(*.${ext})`, `*.${ext}`]],
+ `${Zotero.getString("fileTypes.image").toLowerCase()}.${ext}`,
+ addon.data.imageViewer.window,
+ "images"
+ ).open();
+ if (filename) {
+ await OS.File.writeAtomic(formatPath(filename), u8arr);
+ showHintWithLink(
+ `Image Saved to ${filename}`,
+ "Show in Folder",
+ (ev) => {
+ Zotero.File.reveal(filename);
+ }
+ );
+ }
+ });
+ addon.data.imageViewer.window.document.querySelector("#pin")!.innerHTML =
+ addon.data.imageViewer.pined
+ ? ICONS.imageViewerPined
+ : ICONS.imageViewerPin;
+ addon.data.imageViewer.window.document.querySelector(
+ "#pin-tooltip"
+ )!.innerHTML = addon.data.imageViewer.pined ? "Unpin" : "Pin";
+ addon.data.imageViewer.window.document
+ .querySelector("#pin")
+ ?.addEventListener("click", (e) => {
+ setPin();
+ });
+ addon.data.imageViewer.window.addEventListener("keydown", (e) => {
+ // ctrl+w or esc
+ if ((e.key === "w" && e.ctrlKey) || e.keyCode === 27) {
+ addon.data.imageViewer.window?.close();
+ }
+ addon.data.imageViewer.anchorPosition = {
+ left: img.scrollWidth / 2 - container.scrollLeft / 2,
+ top: img.scrollHeight / 2 - container.scrollLeft / 2,
+ };
+ if (e.keyCode === 37 || e.keyCode === 40) {
+ setIndex("left");
+ }
+ if (e.keyCode === 38 || e.keyCode === 39) {
+ setIndex("right");
+ }
+ if (e.key === "0") {
+ setScale(1);
+ } else if (e.keyCode === 107 || e.keyCode === 187 || e.key === "=") {
+ setScale(addon.data.imageViewer.scaling * 1.1);
+ } else if (e.key === "-") {
+ setScale(addon.data.imageViewer.scaling / 1.1);
+ }
+ });
+ addon.data.imageViewer.window.addEventListener("wheel", async (e) => {
+ addon.data.imageViewer.anchorPosition = {
+ left: e.pageX - container.offsetLeft,
+ top: e.pageY - container.offsetTop,
+ };
+ function normalizeWheelEventDirection(evt: WheelEvent) {
+ let delta = Math.hypot(evt.deltaX, evt.deltaY);
+ const angle = Math.atan2(evt.deltaY, evt.deltaX);
+ if (-0.25 * Math.PI < angle && angle < 0.75 * Math.PI) {
+ // All that is left-up oriented has to change the sign.
+ delta = -delta;
+ }
+ return delta;
+ }
+ const delta = normalizeWheelEventDirection(e);
+ if (e.ctrlKey) {
+ setScale(
+ addon.data.imageViewer.scaling *
+ Math.pow(delta > 0 ? 1.1 : 1 / 1.1, Math.round(Math.abs(delta)))
+ );
+ } else if (e.shiftKey) {
+ container.scrollLeft -= delta * 10;
+ } else {
+ container.scrollLeft += e.deltaX * 10;
+ container.scrollTop += e.deltaY * 10;
+ }
+ });
+ img.addEventListener("mousedown", (e) => {
+ e.preventDefault();
+ // if (addon.data.imageViewer.scaling <= 1) {
+ // return;
+ // }
+ img.onmousemove = (e) => {
+ e.preventDefault();
+ container.scrollLeft -= e.movementX;
+ container.scrollTop -= e.movementY;
+ };
+ img.onmouseleave = () => {
+ img.onmousemove = null;
+ img.onmouseup = null;
+ };
+ img.onmouseup = () => {
+ img.onmousemove = null;
+ img.onmouseup = null;
+ };
+ });
+ }
+
+ addon.data.imageViewer.srcList = srcList;
+ addon.data.imageViewer.idx = idx;
+ addon.data.imageViewer.title = title || "Note";
+ setImage();
+ setScale(1);
+ addon.data.imageViewer.window.focus();
+}
+
+function setImage() {
+ (
+ addon.data.imageViewer.window?.document.querySelector(
+ "#image"
+ ) as HTMLImageElement
+ ).src = addon.data.imageViewer.srcList[addon.data.imageViewer.idx];
+ setTitle();
+ (
+ addon.data.imageViewer.window?.document.querySelector(
+ "#left-container"
+ ) as HTMLButtonElement
+ ).style.opacity = addon.data.imageViewer.idx === 0 ? "0.5" : "1";
+ (
+ addon.data.imageViewer.window?.document.querySelector(
+ "#right-container"
+ ) as HTMLButtonElement
+ ).style.opacity =
+ addon.data.imageViewer.idx === addon.data.imageViewer.srcList.length - 1
+ ? "0.5"
+ : "1";
+}
+
+function setIndex(type: "left" | "right") {
+ if (type === "left") {
+ addon.data.imageViewer.idx > 0
+ ? (addon.data.imageViewer.idx -= 1)
+ : undefined;
+ }
+ if (type === "right") {
+ addon.data.imageViewer.idx < addon.data.imageViewer.srcList.length - 1
+ ? (addon.data.imageViewer.idx += 1)
+ : undefined;
+ }
+ setImage();
+}
+
+function setScale(scaling: number) {
+ const oldScale = addon.data.imageViewer.scaling;
+ addon.data.imageViewer.scaling = scaling;
+ if (addon.data.imageViewer.scaling > 10) {
+ addon.data.imageViewer.scaling = 10;
+ }
+ if (addon.data.imageViewer.scaling < 0.1) {
+ addon.data.imageViewer.scaling = 0.1;
+ }
+ const container = addon.data.imageViewer.window?.document.querySelector(
+ ".container"
+ ) as HTMLDivElement;
+ (
+ addon.data.imageViewer.window?.document.querySelector(
+ "#image"
+ ) as HTMLImageElement
+ ).style.width = `calc(100% * ${addon.data.imageViewer.scaling})`;
+ if (addon.data.imageViewer.scaling > 1) {
+ container.scrollLeft +=
+ addon.data.imageViewer.anchorPosition?.left! *
+ (addon.data.imageViewer.scaling - oldScale);
+ container.scrollTop +=
+ addon.data.imageViewer.anchorPosition?.top! *
+ (addon.data.imageViewer.scaling - oldScale);
+ }
+ (
+ addon.data.imageViewer.window?.document.querySelector(
+ "#bigger-container"
+ ) as HTMLButtonElement
+ ).style.opacity = addon.data.imageViewer.scaling === 10 ? "0.5" : "1";
+ (
+ addon.data.imageViewer.window?.document.querySelector(
+ "#smaller-container"
+ ) as HTMLButtonElement
+ ).style.opacity = addon.data.imageViewer.scaling === 0.1 ? "0.5" : "1";
+ // (
+ // addon.data.imageViewer.window.document.querySelector("#image") as HTMLImageElement
+ // ).style.cursor = addon.data.imageViewer.scaling <= 1 ? "default" : "move";
+}
+
+function setTitle() {
+ addon.data.imageViewer.window!.document.querySelector(
+ "title"
+ )!.innerText! = `${addon.data.imageViewer.idx + 1}/${
+ addon.data.imageViewer.srcList.length
+ }:${addon.data.imageViewer.title}`;
+}
+
+function setPin() {
+ addon.data.imageViewer.window?.close();
+ addon.data.imageViewer.pined = !addon.data.imageViewer.pined;
+ showImageViewer(
+ addon.data.imageViewer.srcList,
+ addon.data.imageViewer.idx,
+ addon.data.imageViewer.title
+ );
+}
diff --git a/src/modules/import/markdown.ts b/src/modules/import/markdown.ts
new file mode 100644
index 0000000..25bf9a3
--- /dev/null
+++ b/src/modules/import/markdown.ts
@@ -0,0 +1,70 @@
+import { addLineToNote } from "../../utils/note";
+
+export async function fromMD(
+ filepath: string,
+ options: {
+ noteId?: number;
+ ignoreVersion?: boolean;
+ append?: boolean;
+ appendLineIndex?: number;
+ } = {}
+) {
+ let mdStatus: MDStatus;
+ try {
+ mdStatus = await addon.api.sync.getMDStatus(filepath);
+ } catch (e) {
+ ztoolkit.log(`Import Error: ${String(e)}`);
+ return;
+ }
+ let noteItem = options.noteId ? Zotero.Items.get(options.noteId) : undefined;
+ if (
+ !options.ignoreVersion &&
+ typeof mdStatus.meta?.version === "number" &&
+ typeof noteItem?.version === "number" &&
+ mdStatus.meta?.version < noteItem?.version
+ ) {
+ if (
+ !window.confirm(
+ `The target note seems to be newer than the file ${filepath}. Are you sure you want to import it anyway?`
+ )
+ ) {
+ return;
+ }
+ }
+ const noteStatus = noteItem
+ ? addon.api.sync.getNoteStatus(noteItem.id)
+ : {
+ meta: '',
+ content: "",
+ tail: "
",
+ };
+
+ if (!noteItem) {
+ noteItem = new Zotero.Item("note");
+ noteItem.libraryID = ZoteroPane.getSelectedLibraryID();
+ if (ZoteroPane.getCollectionTreeRow()?.isCollection()) {
+ noteItem.addToCollection(ZoteroPane.getCollectionTreeRow()?.ref.id);
+ }
+ await noteItem.saveTx({
+ notifierData: {
+ autoSyncDelay: Zotero.Notes.AUTO_SYNC_DELAY,
+ },
+ });
+ }
+ const parsedContent = await addon.api.convert.md2note(mdStatus, noteItem, {
+ isImport: true,
+ });
+ ztoolkit.log("import", noteStatus);
+
+ if (options.append) {
+ await addLineToNote(noteItem, parsedContent, options.appendLineIndex || -1);
+ } else {
+ noteItem.setNote(noteStatus!.meta + parsedContent + noteStatus!.tail);
+ await noteItem.saveTx({
+ notifierData: {
+ autoSyncDelay: Zotero.Notes.AUTO_SYNC_DELAY,
+ },
+ });
+ }
+ return noteItem;
+}
diff --git a/src/modules/menu.ts b/src/modules/menu.ts
new file mode 100644
index 0000000..c9fca77
--- /dev/null
+++ b/src/modules/menu.ts
@@ -0,0 +1,81 @@
+import { config } from "../../package.json";
+import { getString } from "../utils/locale";
+
+export function registerMenus() {
+ // item
+ ztoolkit.Menu.register("item", { tag: "menuseparator" });
+ ztoolkit.Menu.register("item", {
+ tag: "menuitem",
+ label: getString("menuItem.exportNote"),
+ icon: `chrome://${config.addonRef}/content/icons/favicon.png`,
+ commandListener: (ev) => {
+ addon.api.window.showExportNoteOptions(
+ ZoteroPane.getSelectedItems().map((item) => item.id)
+ );
+ },
+ });
+ ztoolkit.Menu.register("item", {
+ tag: "menuitem",
+ label: getString("menuItem.setMainNote"),
+ icon: `chrome://${config.addonRef}/content/icons/favicon.png`,
+ commandListener: (ev) => {
+ addon.hooks.onSetWorkspaceNote(ZoteroPane.getSelectedItems()[0].id);
+ },
+ getVisibility: (elem, ev) => {
+ const items = ZoteroPane.getSelectedItems();
+ return (
+ items.length == 1 &&
+ items[0].isNote() &&
+ items[0].id !== addon.data.workspace.mainId
+ );
+ },
+ });
+
+ // menuEdit
+ const menuEditAnchor = document.querySelector(
+ "#menu_preferences"
+ ) as XUL.MenuItem;
+ ztoolkit.Menu.register(
+ "menuEdit",
+ {
+ tag: "menuitem",
+ label: getString("menuEdit.templatePicker"),
+ icon: `chrome://${config.addonRef}/content/icons/favicon.png`,
+ commandListener: (ev) => {
+ addon.api.window.showTemplatePicker();
+ },
+ },
+ "before",
+ menuEditAnchor
+ );
+ ztoolkit.Menu.register(
+ "menuEdit",
+ {
+ tag: "menuitem",
+ label: getString("menuEdit.templateEditor"),
+ icon: `chrome://${config.addonRef}/content/icons/favicon.png`,
+ commandListener: (ev) => {
+ addon.api.window.showTemplateEditor();
+ },
+ },
+ "before",
+ menuEditAnchor
+ );
+ ztoolkit.Menu.register(
+ "menuEdit",
+ { tag: "menuseparator" },
+ "before",
+ menuEditAnchor
+ );
+
+ // menuTools
+ ztoolkit.Menu.register("menuTools", { tag: "menuseparator" });
+ ztoolkit.Menu.register("menuTools", {
+ tag: "menuitem",
+ label: getString("menuTools.syncManager"),
+ icon: `chrome://${config.addonRef}/content/icons/favicon.png`,
+ commandListener: (ev) => {
+ addon.api.window.showSyncManager();
+ },
+ });
+}
diff --git a/src/modules/noteLink.ts b/src/modules/noteLink.ts
new file mode 100644
index 0000000..653e9f3
--- /dev/null
+++ b/src/modules/noteLink.ts
@@ -0,0 +1,21 @@
+import { getNoteLinkParams } from "../utils/link";
+
+export function registerNoteLinkProxyHandler() {
+ const openNoteExtension = {
+ noContent: true,
+ doAction: async (uri: any) => {
+ const linkParams = getNoteLinkParams(uri.spec);
+ if (linkParams.noteItem) {
+ addon.hooks.onOpenNote(linkParams.noteItem.id, "auto", {
+ lineIndex: linkParams.lineIndex || undefined,
+ });
+ }
+ },
+ newChannel: function (uri: any) {
+ this.doAction(uri);
+ },
+ };
+ Services.io.getProtocolHandler("zotero").wrappedJSObject._extensions[
+ "zotero://note"
+ ] = openNoteExtension;
+}
diff --git a/src/modules/notify.ts b/src/modules/notify.ts
new file mode 100644
index 0000000..6685ec4
--- /dev/null
+++ b/src/modules/notify.ts
@@ -0,0 +1,27 @@
+export function registerNotify(types: _ZoteroTypes.Notifier.Type[]) {
+ const callback = {
+ notify: async (...data: Parameters<_ZoteroTypes.Notifier.Notify>) => {
+ if (!addon?.data.alive) {
+ unregisterNotify(notifyID);
+ return;
+ }
+ addon.hooks.onNotify(...data);
+ },
+ };
+
+ // Register the callback in Zotero as an item observer
+ const notifyID = Zotero.Notifier.registerObserver(callback, types);
+
+ // Unregister callback when the window closes (important to avoid a memory leak)
+ window.addEventListener(
+ "unload",
+ (e: Event) => {
+ unregisterNotify(notifyID);
+ },
+ false
+ );
+}
+
+function unregisterNotify(notifyID: string) {
+ Zotero.Notifier.unregisterObserver(notifyID);
+}
diff --git a/src/modules/preferenceWindow.ts b/src/modules/preferenceWindow.ts
new file mode 100644
index 0000000..7b42f90
--- /dev/null
+++ b/src/modules/preferenceWindow.ts
@@ -0,0 +1,145 @@
+import { config } from "../../package.json";
+import { getString } from "../utils/locale";
+
+export function registerPrefsWindow() {
+ ztoolkit.PreferencePane.register({
+ pluginID: config.addonID,
+ src: rootURI + "chrome/content/preferences.xhtml",
+ label: getString("pref.title"),
+ image: `chrome://${config.addonRef}/content/icons/favicon.png`,
+ extraDTD: [`chrome://${config.addonRef}/locale/overlay.dtd`],
+ defaultXUL: true,
+ });
+}
+
+export function registerPrefsScripts(_window: Window) {
+ // This function is called when the prefs window is opened
+ // See addon/chrome/content/preferences.xul onpaneload
+ if (!addon.data.prefs) {
+ addon.data.prefs = {
+ window: _window,
+ columns: [
+ {
+ dataKey: "title",
+ label: "prefs.table.title",
+ fixedWidth: true,
+ width: 100,
+ },
+ {
+ dataKey: "detail",
+ label: "prefs.table.detail",
+ },
+ ],
+ rows: [
+ {
+ title: "Orange",
+ detail: "It's juicy",
+ },
+ {
+ title: "Banana",
+ detail: "It's sweet",
+ },
+ {
+ title: "Apple",
+ detail: "I mean the fruit APPLE",
+ },
+ ],
+ };
+ } else {
+ addon.data.prefs.window = _window;
+ }
+ updatePrefsUI();
+ bindPrefEvents();
+}
+
+async function updatePrefsUI() {
+ // You can initialize some UI elements on prefs window
+ // with addon.data.prefs.window.document
+ // Or bind some events to the elements
+ const renderLock = ztoolkit.getGlobal("Zotero").Promise.defer();
+ const tableHelper = new ztoolkit.VirtualizedTable(addon.data.prefs?.window!)
+ .setContainerId(`${config.addonRef}-table-container`)
+ .setProp({
+ id: `${config.addonRef}-prefs-table`,
+ // Do not use setLocale, as it modifies the Zotero.Intl.strings
+ // Set locales directly to columns
+ columns: addon.data.prefs?.columns.map((column) =>
+ Object.assign(column, {
+ label: getString(column.label) || column.label,
+ })
+ ),
+ showHeader: true,
+ multiSelect: true,
+ staticColumns: true,
+ disableFontSizeScaling: true,
+ })
+ .setProp("getRowCount", () => addon.data.prefs?.rows.length || 0)
+ .setProp(
+ "getRowData",
+ (index) =>
+ addon.data.prefs?.rows[index] || {
+ title: "no data",
+ detail: "no data",
+ }
+ )
+ // Show a progress window when selection changes
+ .setProp("onSelectionChange", (selection) => {
+ new ztoolkit.ProgressWindow(config.addonName)
+ .createLine({
+ text: `Selected line: ${addon.data.prefs?.rows
+ .filter((v, i) => selection.isSelected(i))
+ .map((row) => row.title)
+ .join(",")}`,
+ progress: 100,
+ })
+ .show();
+ })
+ // When pressing delete, delete selected line and refresh table.
+ // Returning false to prevent default event.
+ .setProp("onKeyDown", (event: KeyboardEvent) => {
+ if (event.key == "Delete" || (Zotero.isMac && event.key == "Backspace")) {
+ addon.data.prefs!.rows =
+ addon.data.prefs?.rows.filter(
+ (v, i) => !tableHelper.treeInstance.selection.isSelected(i)
+ ) || [];
+ tableHelper.render();
+ return false;
+ }
+ return true;
+ })
+ // For find-as-you-type
+ .setProp(
+ "getRowString",
+ (index) => addon.data.prefs?.rows[index].title || ""
+ )
+ // Render the table.
+ .render(-1, () => {
+ renderLock.resolve();
+ });
+ await renderLock.promise;
+ ztoolkit.log("Preference table rendered!");
+}
+
+function bindPrefEvents() {
+ addon.data
+ .prefs!.window.document.querySelector(
+ `#zotero-prefpane-${config.addonRef}-enable`
+ )
+ ?.addEventListener("command", (e) => {
+ ztoolkit.log(e);
+ addon.data.prefs!.window.alert(
+ `Successfully changed to ${(e.target as XUL.Checkbox).checked}!`
+ );
+ });
+
+ addon.data
+ .prefs!!.window.document.querySelector(
+ `#zotero-prefpane-${config.addonRef}-input`
+ )
+ ?.addEventListener("change", (e) => {
+ ztoolkit.log(e);
+ addon.data.prefs!.window.alert(
+ `Successfully changed to ${(e.target as HTMLInputElement).value}!`
+ );
+ });
+}
diff --git a/src/modules/reader.ts b/src/modules/reader.ts
new file mode 100644
index 0000000..5f3f17e
--- /dev/null
+++ b/src/modules/reader.ts
@@ -0,0 +1,391 @@
+import { TagElementProps } from "zotero-plugin-toolkit/dist/tools/ui";
+import { config } from "../../package.json";
+import { ICONS } from "../utils/config";
+import { getNoteLink, getNoteLinkParams } from "../utils/link";
+import { addLineToNote } from "../utils/note";
+
+export function registerReaderInitializer() {
+ ztoolkit.ReaderInstance.register(
+ "initialized",
+ `${config.addonRef}-annotationButtons`,
+ initializeReaderAnnotationButton
+ );
+ // Force re-initialize
+ Zotero.Reader._readers.forEach((r) => {
+ initializeReaderAnnotationButton(r);
+ });
+}
+
+export function unregisterReaderInitializer() {
+ Zotero.Reader._readers.forEach((r) => {
+ unInitializeReaderAnnotationButton(r);
+ });
+}
+
+export async function checkReaderAnnotationButton(items: Zotero.Item[]) {
+ const hitSet = new Set();
+ let t = 0;
+ const period = 100;
+ const wait = 5000;
+ while (items.length > hitSet.size && t < wait) {
+ for (const instance of Zotero.Reader._readers) {
+ const hitItems = await initializeReaderAnnotationButton(instance);
+ hitItems.forEach((item) => hitSet.add(item.id));
+ }
+ await Zotero.Promise.delay(period);
+ t += period;
+ }
+}
+
+async function initializeReaderAnnotationButton(
+ instance: _ZoteroTypes.ReaderInstance
+): Promise {
+ if (!instance) {
+ return [];
+ }
+ await instance._initPromise;
+ await instance._waitForReader();
+ const _document = instance._iframeWindow?.document;
+ if (!_document) {
+ return [];
+ }
+ const hitItems: Zotero.Item[] = [];
+ for (const moreButton of _document.querySelectorAll(".more")) {
+ if (moreButton.getAttribute("_betternotesInitialized") === "true") {
+ continue;
+ }
+ moreButton.setAttribute("_betternotesInitialized", "true");
+
+ let annotationWrapper = moreButton;
+ while (!annotationWrapper.getAttribute("data-sidebar-annotation-id")) {
+ annotationWrapper = annotationWrapper.parentElement!;
+ }
+ const itemKey =
+ annotationWrapper.getAttribute("data-sidebar-annotation-id") || "";
+ if (!instance.itemID) {
+ continue;
+ }
+ const libraryID = Zotero.Items.get(instance.itemID).libraryID;
+ const annotationItem = (await Zotero.Items.getByLibraryAndKeyAsync(
+ libraryID,
+ itemKey
+ )) as Zotero.Item;
+
+ if (!annotationItem) {
+ continue;
+ }
+
+ hitItems.push(annotationItem);
+
+ const annotationButtons: TagElementProps[] = [
+ {
+ tag: "div",
+ classList: ["icon"],
+ properties: {
+ innerHTML: ICONS.readerQuickNote,
+ },
+ listeners: [
+ {
+ type: "click",
+ listener: (e) => {
+ createNoteFromAnnotation(
+ annotationItem,
+ (e as MouseEvent).shiftKey ? "standalone" : "auto"
+ );
+ e.preventDefault();
+ },
+ },
+ {
+ type: "mouseover",
+ listener: (e) => {
+ (e.target as HTMLElement).style.backgroundColor = "#F0F0F0";
+ },
+ },
+ {
+ type: "mouseout",
+ listener: (e) => {
+ (e.target as HTMLElement).style.removeProperty(
+ "background-color"
+ );
+ },
+ },
+ ],
+ enableElementRecord: true,
+ },
+ ];
+
+ if (annotationItem.annotationType === "image") {
+ annotationButtons.push({
+ tag: "div",
+ classList: ["icon"],
+ properties: {
+ innerHTML: ICONS.readerOCR,
+ },
+ listeners: [
+ {
+ type: "click",
+ listener: (e) => {
+ // TODO: OCR
+ e.preventDefault();
+ },
+ },
+ {
+ type: "mouseover",
+ listener: (e) => {
+ (e.target as HTMLElement).style.backgroundColor = "#F0F0F0";
+ },
+ },
+ {
+ type: "mouseout",
+ listener: (e) => {
+ (e.target as HTMLElement).style.removeProperty(
+ "background-color"
+ );
+ },
+ },
+ ],
+ enableElementRecord: true,
+ });
+ }
+
+ ztoolkit.UI.insertElementBefore(
+ {
+ tag: "fragment",
+ children: annotationButtons,
+ },
+ moreButton
+ );
+ }
+ return hitItems;
+}
+
+async function unInitializeReaderAnnotationButton(
+ instance: _ZoteroTypes.ReaderInstance
+): Promise {
+ if (!instance) {
+ return;
+ }
+ await instance._initPromise;
+ await instance._waitForReader();
+ const _document = instance._iframeWindow?.document;
+ if (!_document) {
+ return;
+ }
+ for (const moreButton of _document.querySelectorAll(".more")) {
+ if (moreButton.getAttribute("_betternotesInitialized") === "true") {
+ moreButton.removeAttribute("_betternotesInitialized");
+ }
+ }
+}
+
+async function createNoteFromAnnotation(
+ annotationItem: Zotero.Item,
+ openMode: "standalone" | "auto" = "auto"
+) {
+ const annotationTags = annotationItem.getTags().map((_) => _.tag);
+ const linkRegex = new RegExp("^zotero://note/(.*)$");
+ for (const tag of annotationTags) {
+ if (linkRegex.test(tag)) {
+ const linkParams = getNoteLinkParams(tag);
+ if (linkParams.noteItem) {
+ addon.hooks.onOpenNote(linkParams.noteItem.id, openMode, {
+ lineIndex: linkParams.lineIndex || undefined,
+ });
+ return;
+ } else {
+ annotationItem.removeTag(tag);
+ await annotationItem.saveTx();
+ }
+ }
+ }
+
+ const note: Zotero.Item = new Zotero.Item("note");
+ note.libraryID = annotationItem.libraryID;
+ note.parentID = annotationItem.parentItem!.parentID;
+ await note.saveTx();
+
+ // await waitUtilAsync(() => Boolean(getEditorInstance(note.id)));
+
+ const renderredTemplate = await addon.api.template.runTemplate(
+ "[QuickNoteV5]",
+ "annotationItem, topItem, noteItem",
+ [annotationItem, annotationItem.parentItem!.parentItem, note]
+ );
+ await addLineToNote(note, renderredTemplate);
+
+ const tags = annotationItem.getTags();
+ for (const tag of tags) {
+ note.addTag(tag.tag, tag.type);
+ }
+ await note.saveTx();
+
+ ZoteroPane.openNoteWindow(note.id);
+
+ annotationItem.addTag(getNoteLink(note)!);
+ await annotationItem.saveTx();
+}
+
+// async function OCRImageAnnotation(src: string, annotationItem: Zotero.Item) {
+// /*
+// message.content = {
+// params: { src: string, annotationItem: Zotero.Item }
+// }
+// */
+// let result: string;
+// let success: boolean;
+// const engine = Zotero.Prefs.get("Knowledge4Zotero.OCREngine");
+// if (engine === "mathpix") {
+// const xhr = await Zotero.HTTP.request(
+// "POST",
+// "https://api.mathpix.com/v3/text",
+// {
+// headers: {
+// "Content-Type": "application/json; charset=utf-8",
+// app_id: Zotero.Prefs.get("Knowledge4Zotero.OCRMathpix.Appid"),
+// app_key: Zotero.Prefs.get("Knowledge4Zotero.OCRMathpix.Appkey"),
+// },
+// body: JSON.stringify({
+// src: src,
+// math_inline_delimiters: ["$", "$"],
+// math_display_delimiters: ["$$", "$$"],
+// rm_spaces: true,
+// }),
+// responseType: "json",
+// }
+// );
+// this._Addon.toolkit.Tool.log(xhr);
+// if (xhr && xhr.status && xhr.status === 200 && xhr.response.text) {
+// result = xhr.response.text;
+// success = true;
+// } else {
+// result = xhr.status === 200 ? xhr.response.error : `${xhr.status} Error`;
+// success = false;
+// }
+// } else if (engine === "xunfei") {
+// /**
+// * 1.Doc:https://www.xfyun.cn/doc/words/formula-discern/API.html
+// * 2.Error code:https://www.xfyun.cn/document/error-code
+// * @author iflytek
+// */
+
+// const config = {
+// hostUrl: "https://rest-api.xfyun.cn/v2/itr",
+// host: "rest-api.xfyun.cn",
+// appid: Zotero.Prefs.get("Knowledge4Zotero.OCRXunfei.APPID"),
+// apiSecret: Zotero.Prefs.get("Knowledge4Zotero.OCRXunfei.APISecret"),
+// apiKey: Zotero.Prefs.get("Knowledge4Zotero.OCRXunfei.APIKey"),
+// uri: "/v2/itr",
+// };
+
+// let date = new Date().toUTCString();
+// let postBody = getPostBody();
+// let digest = getDigest(postBody);
+
+// const xhr = await Zotero.HTTP.request("POST", config.hostUrl, {
+// headers: {
+// "Content-Type": "application/json",
+// Accept: "application/json,version=1.0",
+// Host: config.host,
+// Date: date,
+// Digest: digest,
+// Authorization: getAuthStr(date, digest),
+// },
+// body: JSON.stringify(postBody),
+// responseType: "json",
+// });
+
+// if (xhr?.response?.code === 0) {
+// result = xhr.response.data.region
+// .filter((r) => r.type === "text")
+// .map((r) => r.recog.content)
+// .join(" ")
+// .replace(/ifly-latex-(begin)?(end)?/g, "$");
+// this._Addon.toolkit.Tool.log(xhr);
+// success = true;
+// } else {
+// result =
+// xhr.status === 200
+// ? `${xhr.response.code} ${xhr.response.message}`
+// : `${xhr.status} Error`;
+// success = false;
+// }
+
+// function getPostBody() {
+// let digestObj = {
+// common: {
+// app_id: config.appid,
+// },
+// business: {
+// ent: "teach-photo-print",
+// aue: "raw",
+// },
+// data: {
+// image: src.split(",").pop(),
+// },
+// };
+// return digestObj;
+// }
+
+// function getDigest(body) {
+// return (
+// "SHA-256=" +
+// CryptoJS.enc.Base64.stringify(CryptoJS.SHA256(JSON.stringify(body)))
+// );
+// }
+
+// function getAuthStr(date, digest) {
+// let signatureOrigin = `host: ${config.host}\ndate: ${date}\nPOST ${config.uri} HTTP/1.1\ndigest: ${digest}`;
+// let signatureSha = CryptoJS.HmacSHA256(signatureOrigin, config.apiSecret);
+// let signature = CryptoJS.enc.Base64.stringify(signatureSha);
+// let authorizationOrigin = `api_key="${config.apiKey}", algorithm="hmac-sha256", headers="host date request-line digest", signature="${signature}"`;
+// return authorizationOrigin;
+// }
+// } else if (engine === "bing") {
+// const xhr = await Zotero.HTTP.request(
+// "POST",
+// "https://www.bing.com/cameraexp/api/v1/getlatex",
+// {
+// headers: {
+// "Content-Type": "application/json",
+// },
+// body: JSON.stringify({
+// data: src.split(",").pop(),
+// inputForm: "Image",
+// clientInfo: { platform: "edge" },
+// }),
+// responseType: "json",
+// }
+// );
+// if (xhr && xhr.status && xhr.status === 200 && !xhr.response.isError) {
+// result = xhr.response.latex
+// ? `$${xhr.response.latex}$`
+// : xhr.response.ocrText;
+// success = true;
+// } else {
+// result =
+// xhr.status === 200 ? xhr.response.errorMessage : `${xhr.status} Error`;
+// success = false;
+// }
+// } else {
+// result = "OCR Engine Not Found";
+// success = false;
+// }
+// if (success) {
+// annotationItem.annotationComment = `${
+// annotationItem.annotationComment
+// ? `${annotationItem.annotationComment}\n`
+// : ""
+// }${result}`;
+// await annotationItem.saveTx();
+// this._Addon.ZoteroViews.showProgressWindow(
+// "Better Notes OCR",
+// `OCR Result: ${result}`
+// );
+// } else {
+// this._Addon.ZoteroViews.showProgressWindow(
+// "Better Notes OCR",
+// result,
+// "fail"
+// );
+// }
+// }
diff --git a/src/modules/sync/api.ts b/src/modules/sync/api.ts
new file mode 100644
index 0000000..a89cd61
--- /dev/null
+++ b/src/modules/sync/api.ts
@@ -0,0 +1,434 @@
+import YAML = require("yamljs");
+import { showHint } from "../../utils/hint";
+import { getNoteLinkParams } from "../../utils/link";
+import { clearPref, getPref, setPref } from "../../utils/prefs";
+import { getString } from "../../utils/locale";
+
+export {
+ getRelatedNoteIds,
+ removeSyncNote,
+ isSyncNote,
+ getSyncNoteIds,
+ addSyncNote,
+ updateSyncStatus,
+ doSync,
+ setSync,
+ getSyncStatus,
+ getNoteStatus,
+ getMDStatus,
+ getMDStatusFromContent,
+ getMDFileName,
+};
+
+function getSyncNoteIds(): number[] {
+ const ids = getPref("syncNoteIds") as string;
+ return Zotero.Items.get(ids.split(",").map((id: string) => Number(id)))
+ .filter((item) => item.isNote())
+ .map((item) => item.id);
+}
+
+function isSyncNote(noteId: number): boolean {
+ const syncNoteIds = getSyncNoteIds();
+ return syncNoteIds.includes(noteId);
+}
+
+async function getRelatedNoteIds(noteId: number): Promise {
+ let allNoteIds: number[] = [noteId];
+ const note = Zotero.Items.get(noteId);
+ const linkMatches = note.getNote().match(/zotero:\/\/note\/\w+\/\w+\//g);
+ if (!linkMatches) {
+ return allNoteIds;
+ }
+ const subNoteIds = (
+ await Promise.all(
+ linkMatches.map(async (link) => getNoteLinkParams(link).noteItem)
+ )
+ )
+ .filter((item) => item && item.isNote())
+ .map((item) => (item as Zotero.Item).id);
+ allNoteIds = allNoteIds.concat(subNoteIds);
+ allNoteIds = new Array(...new Set(allNoteIds));
+ return allNoteIds;
+}
+
+async function getRelatedNoteIdsFromNotes(
+ noteIds: number[]
+): Promise {
+ let allNoteIds: number[] = [];
+ for (const noteId of noteIds) {
+ allNoteIds = allNoteIds.concat(await getRelatedNoteIds(noteId));
+ }
+ return allNoteIds;
+}
+
+function addSyncNote(noteId: number) {
+ const ids = getSyncNoteIds();
+ if (ids.includes(noteId)) {
+ return;
+ }
+ ids.push(noteId);
+ setPref("syncNoteIds", ids.join(","));
+}
+
+function removeSyncNote(noteId: number) {
+ const ids = getSyncNoteIds();
+ setPref("syncNoteIds", ids.filter((id) => id !== noteId).join(","));
+ clearPref(`syncDetail-${noteId}`);
+}
+
+function updateSyncStatus(noteId: number, status: SyncStatus) {
+ addSyncNote(noteId);
+ setPref(`syncDetail-${noteId}`, JSON.stringify(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 = /"?data-schema-version"?="[0-9]*">/;
+ const match = fullContent?.match(metaRegex);
+ if (!match || match.length == 0) {
+ ret.meta = '';
+ ret.content = fullContent || "";
+ return ret;
+ }
+ const idx = fullContent.search(metaRegex);
+ if (idx != -1) {
+ ret.content = fullContent.substring(
+ idx + match[0].length,
+ fullContent.length - ret.tail.length
+ );
+ }
+ return ret;
+}
+
+function getSyncStatus(noteId?: number): SyncStatus {
+ const defaultStatus = JSON.stringify({
+ path: "",
+ filename: "",
+ md5: "",
+ noteMd5: "",
+ lastsync: new Date().getTime(),
+ itemID: -1,
+ });
+ return JSON.parse(
+ (getPref(`syncDetail-${noteId}`) as string) || defaultStatus
+ );
+}
+
+function getMDStatusFromContent(contentRaw: string): MDStatus {
+ const result = contentRaw.match(/^---([\s\S]*)---\n/);
+ 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 = `${syncStatus.path}/${syncStatus.filename}`;
+ } else if (source.isNote && source.isNote()) {
+ const syncStatus = getSyncStatus(source.id);
+ filepath = `${syncStatus.path}/${syncStatus.filename}`;
+ }
+ filepath = Zotero.File.normalizeToUnix(filepath);
+ if (await OS.File.exists(filepath)) {
+ let contentRaw = (await OS.File.read(filepath, {
+ encoding: "utf-8",
+ })) as string;
+ ret = getMDStatusFromContent(contentRaw);
+ const pathSplit = filepath.split("/");
+ ret.filedir = Zotero.File.normalizeToUnix(
+ pathSplit.slice(0, -1).join("/")
+ );
+ ret.filename = filepath.split("/").pop() || "";
+ const stat = await OS.File.stat(filepath);
+ ret.lastmodify = stat.lastModificationDate;
+ }
+ } catch (e) {
+ ztoolkit.log(e);
+ }
+ return ret;
+}
+
+async function getMDFileName(noteId: number, searchDir?: string) {
+ const noteItem = Zotero.Items.get(noteId);
+ if (searchDir !== undefined && (await OS.File.exists(searchDir))) {
+ const mdRegex = /\.(md|MD|Md|mD)$/;
+ let matchedFileName = null;
+ let matchedDate = new Date(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 OS.File.stat(entry.path);
+ if (stat.lastModificationDate > matchedDate) {
+ matchedFileName = entry.name;
+ matchedDate = stat.lastModificationDate;
+ }
+ }
+ }
+ }
+ );
+ if (matchedFileName) {
+ return matchedFileName;
+ }
+ }
+ return await addon.api.template.runTemplate(
+ "[ExportMDFileNameV2]",
+ "noteItem",
+ [noteItem]
+ );
+}
+
+function setSync() {
+ const syncPeriod = getPref("syncPeriodSeconds") as number;
+ if (syncPeriod > 0) {
+ showHint(`${getString("sync.start.hint")} ${syncPeriod} s`);
+ const timer = ztoolkit.getGlobal("setInterval")(() => {
+ if (!addon.data.alive) {
+ showHint(getString("sync.stop.hint"));
+ ztoolkit.getGlobal("clearInterval")(timer);
+ }
+ // Only when Zotero is active and focused
+ if (document.hasFocus() && (getPref("syncPeriodSeconds") as number) > 0) {
+ doSync(undefined, { quiet: true, skipActive: true, reason: "auto" });
+ }
+ }, Number(syncPeriod) * 1000);
+ }
+}
+
+async function doCompare(noteItem: Zotero.Item): Promise {
+ const syncStatus = getSyncStatus(noteItem.id);
+ const MDStatus = await getMDStatus(noteItem.id);
+ // No file found
+ if (!MDStatus.meta) {
+ return SyncCode.NoteAhead;
+ }
+ // File meta is unavailable
+ if (MDStatus.meta.version < 0) {
+ return SyncCode.NeedDiff;
+ }
+ let MDAhead = false;
+ let noteAhead = false;
+ const md5 = Zotero.Utilities.Internal.md5(MDStatus.content, false);
+ const noteMd5 = Zotero.Utilities.Internal.md5(noteItem.getNote(), false);
+ // MD5 doesn't match (md side change)
+ if (md5 !== syncStatus.md5) {
+ MDAhead = true;
+ }
+ // MD5 doesn't match (note side change)
+ if (noteMd5 !== syncStatus.noteMd5) {
+ noteAhead = true;
+ }
+ // Note version doesn't match (note side change)
+ // This might be unreliable when Zotero account is not login
+ if (Number(MDStatus.meta.version) !== noteItem.version) {
+ noteAhead = true;
+ }
+ if (noteAhead && MDAhead) {
+ return SyncCode.NeedDiff;
+ } else if (noteAhead) {
+ return SyncCode.NoteAhead;
+ } else if (MDAhead) {
+ return SyncCode.MDAhead;
+ } else {
+ return SyncCode.UpToDate;
+ }
+}
+
+async function doSync(
+ items: Zotero.Item[] = [],
+ { quiet, skipActive, reason } = {
+ quiet: true,
+ skipActive: true,
+ reason: "unknown",
+ }
+) {
+ // Always log in development mode
+ if (addon.data.env === "development") {
+ quiet = false;
+ }
+ if (addon.data.sync.lock) {
+ // Only allow one task
+ return;
+ }
+ let progress;
+ // Wrap the code in try...catch so that the lock can be released anyway
+ try {
+ addon.data.sync.lock = true;
+ let skippedCount = 0;
+ if (!items || !items.length) {
+ items = Zotero.Items.get(getSyncNoteIds());
+ }
+ if (skipActive) {
+ // Skip active note editors' targets
+ const activeNoteIds = Zotero.Notes._editorInstances
+ .filter((editor) => editor._iframeWindow.document.hasFocus())
+ .map((editor) => editor._item.id);
+ const filteredItems = items.filter(
+ (item) => !activeNoteIds.includes(item.id)
+ );
+ skippedCount = items.length - filteredItems.length;
+ items = filteredItems;
+ }
+ ztoolkit.log("sync start", reason, items, skippedCount);
+
+ if (!quiet) {
+ progress = new ztoolkit.ProgressWindow(
+ `[${getString("sync.running.hint.title")}] ${
+ addon.data.env === "development" ? reason : "Better Notes"
+ }`
+ )
+ .createLine({
+ text: `[${getString("sync.running.hint.check")}] 0/${
+ items.length
+ } ...`,
+ type: "default",
+ progress: 1,
+ })
+ .show(-1);
+ }
+ // Export items of same dir in batch
+ const toExport = {} as Record;
+ const toImport: SyncStatus[] = [];
+ const toDiff: SyncStatus[] = [];
+ let i = 1;
+ for (const item of items) {
+ const syncStatus = getSyncStatus(item.id);
+ const filepath = syncStatus.path;
+ let compareResult = await doCompare(item);
+ switch (compareResult) {
+ case SyncCode.NoteAhead:
+ if (Object.keys(toExport).includes(filepath)) {
+ toExport[filepath].push(item.id);
+ } else {
+ toExport[filepath] = [item.id];
+ }
+ break;
+ case SyncCode.MDAhead:
+ toImport.push(syncStatus);
+ break;
+ case SyncCode.NeedDiff:
+ toDiff.push(syncStatus);
+ break;
+ default:
+ break;
+ }
+ progress?.changeLine({
+ text: `[${getString("sync.running.hint.check")}] ${i}/${
+ items.length
+ } ...`,
+ progress: ((i - 1) / items.length) * 100,
+ });
+ i += 1;
+ }
+ ztoolkit.log("will be synced:", toExport, toImport, toDiff);
+ i = 1;
+ let totalCount = Object.keys(toExport).length;
+ for (const filepath of Object.keys(toExport)) {
+ progress?.changeLine({
+ text: `[${getString("sync.running.hint.updateMD")}] ${i}/${
+ items.length
+ } ...`,
+ progress: ((i - 1) / items.length) * 100,
+ });
+ await addon.api._export.syncMDBatch(filepath, toExport[filepath]);
+ i += 1;
+ }
+ i = 1;
+ totalCount = toImport.length;
+ for (const syncStatus of toImport) {
+ progress?.changeLine({
+ text: `[${getString(
+ "sync.running.hint.updateNote"
+ )}] ${i}/${totalCount}, ${toDiff.length} queuing...`,
+ progress: ((i - 1) / totalCount) * 100,
+ });
+ const item = Zotero.Items.get(syncStatus.itemID);
+ const filepath = OS.Path.join(syncStatus.path, syncStatus.filename);
+ await addon.api._import.fromMD(filepath, { noteId: item.id });
+ // Update md file to keep the metadata synced
+ await addon.api._export.syncMDBatch(syncStatus.path, [item.id]);
+ i += 1;
+ }
+ i = 1;
+ totalCount = toDiff.length;
+ for (const syncStatus of toDiff) {
+ progress?.changeLine({
+ text: `[${getString("sync.running.hint.diff")}] ${i}/${totalCount}...`,
+ progress: ((i - 1) / totalCount) * 100,
+ });
+
+ await addon.api.window.showSyncDiff(
+ syncStatus.itemID,
+ OS.Path.join(syncStatus.path, syncStatus.filename)
+ );
+ i += 1;
+ }
+ const syncCount =
+ Object.keys(toExport).length + toImport.length + toDiff.length;
+ progress?.changeLine({
+ text:
+ (syncCount
+ ? `[${getString(
+ "sync.running.hint.finish"
+ )}] ${syncCount} ${getString("sync.running.hint.synced")}`
+ : `[${getString("sync.running.hint.finish")}] ${getString(
+ "sync.running.hint.upToDate"
+ )}`) + (skippedCount ? `, ${skippedCount} skipped.` : ""),
+ progress: 100,
+ });
+ } catch (e) {
+ ztoolkit.log(e);
+ showHint(`Sync Error: ${String(e)}`);
+ } finally {
+ progress?.startCloseTimer(5000);
+ }
+ addon.data.sync.lock = false;
+}
+
+enum SyncCode {
+ UpToDate = 0,
+ NoteAhead,
+ MDAhead,
+ NeedDiff,
+}
diff --git a/src/modules/sync/diffWindow.ts b/src/modules/sync/diffWindow.ts
new file mode 100644
index 0000000..476b086
--- /dev/null
+++ b/src/modules/sync/diffWindow.ts
@@ -0,0 +1,141 @@
+import { diffChars } from "diff";
+import { config } from "../../../package.json";
+import { getItemDataURL } from "../../utils/str";
+import { isWindowAlive } from "../../utils/window";
+import { waitUtilAsync } from "../../utils/wait";
+
+export async function showSyncDiff(noteId: number, mdPath: string) {
+ const noteItem = Zotero.Items.get(noteId);
+ const syncStatus = addon.api.sync.getSyncStatus(noteId);
+ const noteStatus = addon.api.sync.getNoteStatus(noteId)!;
+ mdPath = Zotero.File.normalizeToUnix(mdPath);
+ if (!noteItem || !noteItem.isNote() || !(await OS.File.exists(mdPath))) {
+ return;
+ }
+ const mdStatus = await addon.api.sync.getMDStatus(mdPath);
+ if (!mdStatus.meta) {
+ return;
+ }
+ const mdNoteContent = await addon.api.convert.md2note(mdStatus, noteItem, {
+ isImport: true,
+ });
+ const noteContent = await addon.api.convert.note2noteDiff(noteItem);
+ ztoolkit.log(mdNoteContent, noteContent);
+ const changes = diffChars(noteContent, mdNoteContent);
+ ztoolkit.log("changes", changes);
+
+ const io = {
+ defer: Zotero.Promise.defer(),
+ result: "",
+ type: "skip",
+ };
+
+ const syncDate = new Date(syncStatus.lastsync);
+ if (!(noteStatus.lastmodify > syncDate && mdStatus.lastmodify > syncDate)) {
+ // If only one kind of changes, merge automatically
+ if (noteStatus.lastmodify >= mdStatus.lastmodify) {
+ // refuse all, keep note
+ io.result = changes
+ .filter((diff) => (!diff.added && !diff.removed) || diff.removed)
+ .map((diff) => diff.value)
+ .join("");
+ } else {
+ // accept all, keep md
+ io.result = changes
+ .filter((diff) => (!diff.added && !diff.removed) || diff.added)
+ .map((diff) => diff.value)
+ .join("");
+ }
+ io.type = "finish";
+ } else {
+ // Otherwise, merge manually
+ const imageAttachemnts = Zotero.Items.get(noteItem.getAttachments()).filter(
+ (attch) => attch.isEmbeddedImageAttachment()
+ );
+ const imageData = {} as Record;
+ for (const image of imageAttachemnts) {
+ try {
+ const b64 = await getItemDataURL(image);
+ imageData[image.key] = b64;
+ } catch (e) {
+ ztoolkit.log(e);
+ }
+ }
+
+ if (!isWindowAlive(addon.data.sync.diff.window)) {
+ addon.data.sync.diff.window = window.open(
+ `chrome://${config.addonRef}/content/syncDiff.html`,
+ `${config.addonRef}-syncDiff`,
+ `chrome,centerscreen,resizable,status,width=900,height=550`
+ )!;
+ await waitUtilAsync(
+ () => addon.data.sync.diff.window?.document.readyState === "complete"
+ );
+ }
+ const win = addon.data.sync.diff.window as any;
+
+ win.document.title = `[Better Notes Sycing] Diff Merge of ${noteItem.getNoteTitle()}`;
+ win.syncInfo = {
+ noteName: noteItem.getNoteTitle(),
+ noteModify: noteStatus.lastmodify.toISOString(),
+ mdName: mdPath,
+ mdModify: mdStatus.lastmodify.toISOString(),
+ syncTime: syncDate.toISOString(),
+ };
+ win.diffData = changes.map((change, id) =>
+ Object.assign(change, {
+ id: id,
+ text: change.value,
+ })
+ );
+ win.imageData = imageData;
+
+ win.io = io;
+ win.initSyncInfo();
+ win.initList();
+ win.initDiffViewer();
+ win.updateDiffRender([]);
+ const abort = () => {
+ ztoolkit.log("unloaded");
+ io.defer.resolve();
+ };
+ // If closed by user, abort syncing
+ win.addEventListener("beforeunload", abort);
+ win.addEventListener("unload", abort);
+ win.addEventListener("close", abort);
+ win.onclose = abort;
+ win.onbeforeunload = abort;
+ win.onunload = abort;
+ await io.defer.promise;
+ win.removeEventListener("beforeunload", abort);
+ win.removeEventListener("unload", abort);
+ win.removeEventListener("close", abort);
+ }
+
+ switch (io.type) {
+ case "skip":
+ alert(
+ `Syncing of "${noteItem.getNoteTitle()}" is skipped.\nTo sync manually, go to File->Better Notes Sync Manager.`
+ );
+ addon.data.sync.diff.window?.closed ||
+ addon.data.sync.diff.window?.close();
+ break;
+ case "unsync":
+ ztoolkit.log("remove sync", noteItem.getNote());
+ await addon.api.sync.removeSyncNote(noteItem.id);
+ break;
+ case "finish":
+ ztoolkit.log("Diff result:", io.result);
+ // return io.result;
+ noteItem.setNote(noteStatus.meta + io.result + noteStatus.tail);
+ await noteItem.saveTx({
+ notifierData: {
+ autoSyncDelay: Zotero.Notes.AUTO_SYNC_DELAY,
+ },
+ });
+ await addon.api._export.syncMDBatch(mdStatus.filedir, [noteItem.id]);
+ break;
+ default:
+ break;
+ }
+}
diff --git a/src/modules/sync/infoWindow.ts b/src/modules/sync/infoWindow.ts
new file mode 100644
index 0000000..e9eb16f
--- /dev/null
+++ b/src/modules/sync/infoWindow.ts
@@ -0,0 +1,81 @@
+import { showHint } from "../../utils/hint";
+import { getString } from "../../utils/locale";
+import { formatPath, slice } from "../../utils/str";
+
+export async function showSyncInfo(noteId: number) {
+ const status = addon.api.sync.getSyncStatus(noteId);
+ const data = {} as Record;
+
+ const dialog = new ztoolkit.Dialog(4, 1)
+ .setDialogData(data)
+ .addCell(0, 0, {
+ tag: "h3",
+ properties: {
+ innerHTML: getString("syncInfo.syncTo"),
+ },
+ })
+ .addCell(1, 0, {
+ tag: "label",
+ properties: {
+ innerHTML: formatPath(
+ OS.Path.join(slice(status.path, 30), status.filename)
+ ),
+ },
+ })
+ .addCell(2, 0, {
+ tag: "h3",
+ properties: {
+ innerHTML: getString("syncInfo.lastSync"),
+ },
+ })
+ .addCell(3, 0, {
+ tag: "label",
+ properties: {
+ innerHTML: new Date(status.lastsync).toLocaleString(),
+ },
+ })
+ .addButton(getString("syncInfo.sync"), "sync", {
+ noClose: true,
+ callback: (ev) => {
+ addon.api.sync.doSync(undefined, {
+ quiet: false,
+ skipActive: false,
+ reason: "manual-info",
+ });
+ },
+ })
+ .addButton(getString("syncInfo.unSync"), "unSync", {
+ callback: async (ev) => {
+ const allNoteIds = await addon.api.sync.getRelatedNoteIds(noteId);
+ for (const itemId of allNoteIds) {
+ addon.api.sync.removeSyncNote(itemId);
+ }
+ showHint(`Cancel sync of ${allNoteIds.length} notes.`);
+ },
+ })
+ .addButton(getString("syncInfo.reveal"), "reveal", {
+ noClose: true,
+ callback: (ev) => {
+ Zotero.File.reveal(
+ formatPath(OS.Path.join(status.path, status.filename))
+ );
+ },
+ })
+ .addButton(getString("syncInfo.manager"), "manager", {
+ noClose: true,
+ callback: (ev) => {
+ addon.api.window.showSyncManager();
+ },
+ })
+ .addButton(getString("syncInfo.export"), "export", {
+ callback: (ev) => {
+ addon.api.window.showExportNoteOptions([noteId]);
+ },
+ })
+ .addButton(getString("export.cancel"), "cancel")
+ .open(getString("export.title"), {
+ resizable: true,
+ centerscreen: true,
+ fitContent: true,
+ });
+}
diff --git a/src/modules/sync/managerWindow.ts b/src/modules/sync/managerWindow.ts
new file mode 100644
index 0000000..249cfdd
--- /dev/null
+++ b/src/modules/sync/managerWindow.ts
@@ -0,0 +1,197 @@
+import { config } from "../../../package.json";
+import { getLinkedNotesRecursively, getNoteLink } from "../../utils/link";
+import { getString } from "../../utils/locale";
+import { isWindowAlive, localeWindow } from "../../utils/window";
+
+export async function showSyncManager() {
+ if (isWindowAlive(addon.data.sync.manager.window)) {
+ addon.data.sync.manager.window?.focus();
+ refresh();
+ } else {
+ const windowArgs = {
+ _initPromise: Zotero.Promise.defer(),
+ };
+ const win = window.openDialog(
+ `chrome://${config.addonRef}/content/syncManager.xhtml`,
+ `${config.addonRef}-syncManager`,
+ `chrome,centerscreen,resizable,status,width=800,height=400,dialog=no`,
+ windowArgs
+ )!;
+ await windowArgs._initPromise.promise;
+ addon.data.sync.manager.window = win;
+ localeWindow(win);
+ updateData();
+ addon.data.sync.manager.tableHelper = new ztoolkit.VirtualizedTable(win!)
+ .setContainerId("table-container")
+ .setProp({
+ id: "manager-table",
+ // Do not use setLocale, as it modifies the Zotero.Intl.strings
+ // Set locales directly to columns
+ columns: [
+ {
+ dataKey: "noteName",
+ label: "syncManager.noteName",
+ fixedWidth: false,
+ },
+ {
+ dataKey: "lastSync",
+ label: "syncManager.lastSync",
+ fixedWidth: false,
+ },
+ {
+ dataKey: "filePath",
+ label: "syncManager.filePath",
+ fixedWidth: false,
+ },
+ ].map((column) =>
+ Object.assign(column, {
+ label: getString(column.label),
+ })
+ ),
+ showHeader: true,
+ multiSelect: true,
+ staticColumns: true,
+ disableFontSizeScaling: true,
+ })
+ .setProp("getRowCount", () => addon.data.sync.manager.data.length)
+ .setProp(
+ "getRowData",
+ (index) =>
+ (addon.data.sync.manager.data[index] as {
+ noteName: string;
+ lastSync: string;
+ filePath: string;
+ }) || {
+ noteName: "no data",
+ lastSync: "no data",
+ filePath: "no data",
+ }
+ )
+ .setProp("onSelectionChange", (selection) => {
+ updateButtons();
+ })
+ .setProp("onKeyDown", (event: KeyboardEvent) => {
+ if (
+ event.key == "Delete" ||
+ (Zotero.isMac && event.key == "Backspace")
+ ) {
+ unSyncNotes(getSelectedNoteIds());
+ refresh();
+ return false;
+ }
+ return true;
+ })
+ .setProp("onActivate", (ev) => {
+ const noteIds = getSelectedNoteIds();
+ noteIds.forEach((noteId) =>
+ addon.hooks.onOpenNote(noteId, "standalone")
+ );
+ return true;
+ })
+ .setProp(
+ "getRowString",
+ (index) => addon.data.prefs?.rows[index].title || ""
+ )
+ .render();
+ const refreshButton = win.document.querySelector(
+ "#refresh"
+ ) as HTMLButtonElement;
+ const syncButton = win.document.querySelector("#sync") as HTMLButtonElement;
+ const unSyncButton = win.document.querySelector(
+ "#unSync"
+ ) as HTMLButtonElement;
+ refreshButton.addEventListener("click", (ev) => {
+ refresh();
+ });
+ syncButton.addEventListener("click", async (ev) => {
+ await addon.api.sync.doSync(Zotero.Items.get(getSelectedNoteIds()), {
+ quiet: false,
+ skipActive: false,
+ reason: "manual-manager",
+ });
+ refresh();
+ });
+ unSyncButton.addEventListener("click", (ev) => {
+ getSelectedNoteIds().forEach((noteId) => {
+ addon.api.sync.removeSyncNote(noteId);
+ });
+ refresh();
+ });
+ }
+}
+
+function updateData() {
+ addon.data.sync.manager.data = addon.api.sync
+ .getSyncNoteIds()
+ .map((noteId) => {
+ const syncStatus = addon.api.sync.getSyncStatus(noteId);
+ return {
+ noteId: noteId,
+ noteName: Zotero.Items.get(noteId).getNoteTitle(),
+ lastSync: new Date(syncStatus.lastsync).toLocaleString(),
+ filePath: OS.Path.join(syncStatus.path, syncStatus.filename),
+ };
+ });
+}
+
+async function updateTable() {
+ return new Promise((resolve) => {
+ addon.data.sync.manager.tableHelper?.render(undefined, (_) => {
+ resolve();
+ });
+ });
+}
+
+function updateButtons() {
+ const win = addon.data.sync.manager.window;
+ if (!win) {
+ return;
+ }
+ const unSyncButton = win.document.querySelector(
+ "#unSync"
+ ) as HTMLButtonElement;
+ if (
+ addon.data.sync.manager.tableHelper?.treeInstance.selection.selected.size
+ ) {
+ unSyncButton.disabled = false;
+ } else {
+ unSyncButton.disabled = true;
+ }
+}
+
+async function refresh() {
+ updateData();
+ await updateTable();
+ updateButtons();
+}
+
+function getSelectedNoteIds() {
+ const ids = [];
+ for (const idx of addon.data.sync.manager.tableHelper?.treeInstance.selection.selected?.keys() ||
+ []) {
+ ids.push(addon.data.sync.manager.data[idx].noteId);
+ }
+ return ids;
+}
+
+async function unSyncNotes(itemIds: number[]) {
+ if (itemIds.length === 0) {
+ return;
+ }
+ const unSyncLinkedNotes = addon.data.sync.manager.window?.confirm(
+ `Un-sync their linked notes?`
+ );
+ if (unSyncLinkedNotes) {
+ for (const item of Zotero.Items.get(itemIds)) {
+ let linkedIds: number[] = getLinkedNotesRecursively(
+ getNoteLink(item) || "",
+ itemIds
+ );
+ itemIds.push(...linkedIds);
+ }
+ }
+ for (const itemId of itemIds) {
+ await addon.api.sync.removeSyncNote(itemId);
+ }
+ refresh();
+}
diff --git a/src/modules/template/api.ts b/src/modules/template/api.ts
new file mode 100644
index 0000000..a32c06a
--- /dev/null
+++ b/src/modules/template/api.ts
@@ -0,0 +1,165 @@
+import { itemPicker } from "../../utils/itemPicker";
+import { copyEmbeddedImagesInHTML, renderNoteHTML } from "../../utils/note";
+
+export { runTemplate, runItemTemplate };
+
+const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
+
+async function runTemplate(
+ key: string,
+ argString: string = "",
+ argList: any[] = [],
+ options: {
+ useDefault?: boolean;
+ dryRun?: boolean;
+ stage?: string;
+ } = {
+ useDefault: true,
+ dryRun: false,
+ stage: "default",
+ }
+): Promise {
+ ztoolkit.log(`runTemplate: ${key}`);
+ argString += ", _env";
+ argList.push({
+ dryRun: options.dryRun,
+ });
+ let templateText = addon.api.template.getTemplateText(key);
+ if (options.useDefault && !templateText) {
+ templateText =
+ addon.api.template.DEFAULT_TEMPLATES.find((t) => t.name === key)?.text ||
+ "";
+ if (!templateText) {
+ return "";
+ }
+ }
+
+ let templateLines = templateText.split("\n");
+ let startIndex = templateLines.indexOf(`// @${options.stage}-begin`),
+ endIndex = templateLines.indexOf(`// @${options.stage}-end`);
+ if (
+ startIndex < 0 &&
+ endIndex < 0 &&
+ typeof options.stage === "string" &&
+ options.stage !== "default"
+ ) {
+ // Skip this stage
+ return "";
+ }
+ if (startIndex < 0) {
+ // We skip the flag line later
+ startIndex = -1;
+ }
+ if (endIndex < 0) {
+ endIndex = templateLines.length;
+ }
+ // Skip the flag lines
+ templateLines = templateLines.slice(startIndex + 1, endIndex);
+ let useMarkdown = false;
+ let mdIndex = templateLines.indexOf("// @use-markdown");
+ if (mdIndex >= 0) {
+ useMarkdown = true;
+ templateLines.splice(mdIndex, 1);
+ }
+ templateText = templateLines.join("\n");
+
+ try {
+ const func = new AsyncFunction(argString, "return `" + templateText + "`");
+ const res = await func(...argList);
+ ztoolkit.log(res);
+ return useMarkdown ? await addon.api.convert.md2html(res) : res;
+ } catch (e) {
+ ztoolkit.log(e);
+ if (options.dryRun) {
+ return String(e);
+ }
+ window.alert(`Template ${key} Error: ${e}`);
+ return "";
+ }
+}
+
+async function runItemTemplate(
+ key: string,
+ options: {
+ itemIds?: number[];
+ targetNoteId?: number;
+ dryRun?: boolean;
+ }
+): Promise {
+ /**
+ * args:
+ * beforeloop stage: items, copyNoteImage, sharedObj(for temporary variables, shared by all stages)
+ * default stage: topItem, itemNotes, copyNoteImage, sharedObj
+ * afterloop stage: items, copyNoteImage, sharedObj
+ */
+ let { itemIds, targetNoteId, dryRun } = options;
+ if (!itemIds) {
+ itemIds = await itemPicker();
+ }
+
+ const items = itemIds?.map((id) => Zotero.Items.get(id)) || [];
+
+ const copyImageRefNotes: Zotero.Item[] = [];
+ const copyNoteImage = (noteItem: Zotero.Item) => {
+ copyImageRefNotes.push(noteItem);
+ };
+
+ const sharedObj = {};
+
+ const results = [];
+
+ results.push(
+ await runTemplate(
+ key,
+ "items, copyNoteImage, sharedObj",
+ [items, copyNoteImage, sharedObj],
+ {
+ stage: "beforeloop",
+ useDefault: false,
+ dryRun,
+ }
+ )
+ );
+
+ for (const topItem of items) {
+ const itemNotes = topItem.isRegularItem()
+ ? Zotero.Items.get(topItem.getNotes())
+ : [];
+ results.push(
+ await runTemplate(
+ key,
+ "topItem, itemNotes, copyNoteImage, sharedObj",
+ [topItem, itemNotes, copyNoteImage, sharedObj],
+ {
+ dryRun,
+ }
+ )
+ );
+ }
+
+ results.push(
+ await runTemplate(
+ key,
+ "items, copyNoteImage, sharedObj",
+ [items, copyNoteImage, sharedObj],
+ {
+ stage: "afterloop",
+ useDefault: false,
+ dryRun,
+ }
+ )
+ );
+
+ let html = results.join("\n");
+ const targetNoteItem = Zotero.Items.get(targetNoteId || -1);
+ if (targetNoteItem && targetNoteItem.isNote()) {
+ html = await copyEmbeddedImagesInHTML(
+ html,
+ targetNoteItem,
+ copyImageRefNotes
+ );
+ } else {
+ html = await renderNoteHTML(html, copyImageRefNotes);
+ }
+ return html;
+}
diff --git a/src/modules/template/controller.ts b/src/modules/template/controller.ts
new file mode 100644
index 0000000..e154877
--- /dev/null
+++ b/src/modules/template/controller.ts
@@ -0,0 +1,85 @@
+import { clearPref, getPref, setPref } from "../../utils/prefs";
+
+export {
+ getTemplateKeys,
+ getTemplateText,
+ setTemplate,
+ initTemplates,
+ removeTemplate,
+};
+
+// Controller
+function getTemplateKeys(): { name: string }[] {
+ let templateKeys = getPref("templateKeys") as string;
+ return templateKeys ? JSON.parse(templateKeys) : [];
+}
+
+function setTemplateKeys(templateKeys: { name: string }[]): void {
+ setPref("templateKeys", JSON.stringify(templateKeys));
+}
+
+function addTemplateKey(templateKey: { name: string }): boolean {
+ const templateKeys = getTemplateKeys();
+ if (templateKeys.map((t) => t.name).includes(templateKey.name)) {
+ return false;
+ }
+ templateKeys.push(templateKey);
+ setTemplateKeys(templateKeys);
+ return true;
+}
+
+function removeTemplateKey(keyName: string): boolean {
+ const templateKeys = getTemplateKeys();
+ if (!templateKeys.map((t) => t.name).includes(keyName)) {
+ return false;
+ }
+ templateKeys.splice(templateKeys.map((t) => t.name).indexOf(keyName), 1);
+ setTemplateKeys(templateKeys);
+ return true;
+}
+
+function getTemplateText(keyName: string): string {
+ let template = getPref(`template.${keyName}`) as string;
+ if (!template) {
+ template = "";
+ setPref(`template.${keyName}`, template);
+ }
+ return template;
+}
+
+function setTemplate(
+ template: NoteTemplate,
+ updatePrompt: boolean = true
+): void {
+ template = JSON.parse(JSON.stringify(template));
+ addTemplateKey({ name: template.name });
+ setPref(`template.${template.name}`, template.text);
+ if (updatePrompt) {
+ addon.api.template.updateTemplatePicker();
+ }
+}
+
+function removeTemplate(
+ keyName: string | undefined,
+ updatePrompt: boolean = true
+): void {
+ if (typeof keyName === "undefined") {
+ return;
+ }
+ removeTemplateKey(keyName);
+ clearPref(`template.${keyName}`);
+ if (updatePrompt) {
+ addon.api.template.updateTemplatePicker();
+ }
+}
+
+function initTemplates() {
+ let templateKeys = getTemplateKeys();
+ const currentNames = templateKeys.map((t) => t.name);
+ for (const defaultTemplate of addon.api.template.DEFAULT_TEMPLATES) {
+ if (!currentNames.includes(defaultTemplate.name)) {
+ setTemplate(defaultTemplate, false);
+ }
+ }
+ addon.api.template.updateTemplatePicker();
+}
diff --git a/src/modules/template/data.ts b/src/modules/template/data.ts
new file mode 100644
index 0000000..e74469b
--- /dev/null
+++ b/src/modules/template/data.ts
@@ -0,0 +1,73 @@
+// Data
+export { SYSTEM_TEMPLATE_NAMES, DEFAULT_TEMPLATES };
+
+const SYSTEM_TEMPLATE_NAMES = [
+ "[QuickInsertV2]",
+ "[QuickBackLinkV2]",
+ "[QuickImportV2]",
+ "[QuickNoteV5]",
+ "[ExportMDFileNameV2]",
+ "[ExportMDFileHeaderV2]",
+];
+
+// Non-system templates are removed from default templates
+const DEFAULT_TEMPLATES = [
+ {
+ name: "[QuickInsertV2]",
+ text: `
+
+ \${linkText}
+
+
`,
+ },
+ {
+ name: "[QuickBackLinkV2]",
+ text: `
+ Referred in
+
+ \${linkText}
+
+
`,
+ },
+ {
+ name: "[QuickImportV2]",
+ text: `
+ \${await new Promise(async (r) => {
+ r(await Zotero.BetterNotes.api.convert.link2html(link, {noteItem, dryRun: _env.dryRun}));
+ })}
+ `,
+ },
+ {
+ name: "[QuickNoteV5]",
+ text: `\${await new Promise(async (r) => {
+ let res = "";
+ if (annotationItem.annotationComment) {
+ res += await Zotero.BetterNotes.api.convert.md2html(
+ annotationItem.annotationComment
+ );
+ }
+ res += await Zotero.BetterNotes.api.convert.annotations2html([annotationItem], {noteItem, ignoreComment: true});
+ r(res);
+ })}`,
+ },
+ {
+ name: "[ExportMDFileNameV2]",
+ text: '${(noteItem.getNoteTitle ? noteItem.getNoteTitle().replace(/[/\\\\?%*:|"<> ]/g, "-") + "-" : "")}${noteItem.key}.md',
+ },
+ {
+ name: "[ExportMDFileHeaderV2]",
+ text: `\${await new Promise(async (r) => {
+ let header = {};
+ header.tags = noteItem.getTags().map((_t) => _t.tag);
+ header.parent = noteItem.parentItem
+ ? noteItem.parentItem.getField("title")
+ : "";
+ header.collections = (
+ await Zotero.Collections.getCollectionsContainingItems([
+ (noteItem.parentItem || noteItem).id,
+ ])
+ ).map((c) => c.name);
+ r(JSON.stringify(header));
+ })}`,
+ },
+];
diff --git a/src/modules/template/editorWindow.ts b/src/modules/template/editorWindow.ts
new file mode 100644
index 0000000..42abb7a
--- /dev/null
+++ b/src/modules/template/editorWindow.ts
@@ -0,0 +1,284 @@
+import { config } from "../../../package.json";
+import { showHint } from "../../utils/hint";
+import { itemPicker } from "../../utils/itemPicker";
+import { getString } from "../../utils/locale";
+import { localeWindow } from "../../utils/window";
+
+export async function showTemplateEditor() {
+ if (
+ !addon.data.templateEditor.window ||
+ Components.utils.isDeadWrapper(addon.data.templateEditor.window) ||
+ addon.data.templateEditor.window.closed
+ ) {
+ const windowArgs = {
+ _initPromise: Zotero.Promise.defer(),
+ };
+ // @ts-ignore
+ const _window = window.openDialog(
+ `chrome://${config.addonRef}/content/templateEditor.xhtml`,
+ `${config.addonRef}-templateEditor`,
+ `chrome,centerscreen,resizable,status,width=800,height=400,dialog=no`,
+ windowArgs
+ )!;
+ addon.data.templateEditor.window = _window;
+ await windowArgs._initPromise.promise;
+ localeWindow(_window);
+ updateData();
+ addon.data.templateEditor.tableHelper = new ztoolkit.VirtualizedTable(
+ _window!
+ )
+ .setContainerId("table-container")
+ .setProp({
+ id: "templates-table",
+ // Do not use setLocale, as it modifies the Zotero.Intl.strings
+ // Set locales directly to columns
+ columns: [
+ {
+ dataKey: "name",
+ label: "templateEditor.templateName",
+ fixedWidth: false,
+ },
+ ].map((column) =>
+ Object.assign(column, {
+ label: getString(column.label),
+ })
+ ),
+ showHeader: true,
+ multiSelect: false,
+ staticColumns: true,
+ disableFontSizeScaling: true,
+ })
+ .setProp("getRowCount", () => addon.data.templateEditor.templates.length)
+ .setProp(
+ "getRowData",
+ (index) =>
+ (addon.data.templateEditor.templates[index] as { name: string }) || {
+ name: "no data",
+ }
+ )
+ .setProp("onSelectionChange", (selection) => {
+ updateEditor();
+ updatePreview();
+ })
+ .setProp("onKeyDown", (event: KeyboardEvent) => {
+ if (
+ event.key == "Delete" ||
+ (Zotero.isMac && event.key == "Backspace")
+ ) {
+ addon.api.template.removeTemplate(getSelectedTemplateName());
+ refresh();
+ return false;
+ }
+ return true;
+ })
+ .setProp(
+ "getRowString",
+ (index) => addon.data.prefs?.rows[index].title || ""
+ )
+ .render();
+ _window.document
+ .querySelector("#create")
+ ?.addEventListener("click", (ev) => {
+ createTemplate();
+ });
+ _window.document
+ .querySelector("#import")
+ ?.addEventListener("click", (ev) => {
+ importNoteTemplate();
+ });
+ _window.document.querySelector("#help")?.addEventListener("click", (ev) => {
+ Zotero.launchURL(
+ "https://github.com/windingwind/zotero-better-notes/blob/master/Template.md"
+ );
+ });
+ _window.document.querySelector("#more")?.addEventListener("click", (ev) => {
+ Zotero.launchURL(
+ "https://github.com/windingwind/zotero-better-notes/discussions/categories/note-templates"
+ );
+ });
+ _window.document.querySelector("#save")?.addEventListener("click", (ev) => {
+ saveSelectedTemplate();
+ });
+ _window.document
+ .querySelector("#delete")
+ ?.addEventListener("click", (ev) => {
+ deleteSelectedTemplate();
+ });
+ _window.document
+ .querySelector("#reset")
+ ?.addEventListener("click", (ev) => {
+ resetSelectedTemplate();
+ });
+ }
+ addon.data.templateEditor.window?.focus();
+}
+
+async function refresh() {
+ updateData();
+ updateTable();
+ updateEditor();
+ await updatePreview();
+}
+
+function updateData() {
+ addon.data.templateEditor.templates = addon.api.template.getTemplateKeys();
+}
+
+function updateTable(selectId?: number) {
+ addon.data.templateEditor.tableHelper?.render(selectId);
+}
+
+function updateEditor() {
+ const name = getSelectedTemplateName();
+ const templateText = addon.api.template.getTemplateText(name);
+
+ const header = addon.data.templateEditor.window?.document.getElementById(
+ "editor-name"
+ ) as HTMLInputElement;
+ const text = addon.data.templateEditor.window?.document.getElementById(
+ "editor-textbox"
+ ) as HTMLTextAreaElement;
+ const saveTemplate =
+ addon.data.templateEditor.window?.document.getElementById(
+ "save"
+ ) as XUL.Button;
+ const deleteTemplate =
+ addon.data.templateEditor.window?.document.getElementById(
+ "delete"
+ ) as XUL.Button;
+ const resetTemplate =
+ addon.data.templateEditor.window?.document.getElementById(
+ "reset"
+ ) as XUL.Button;
+ if (!name) {
+ header.value = "";
+ header.setAttribute("disabled", "true");
+ text.value = "";
+ text.setAttribute("disabled", "true");
+ saveTemplate.setAttribute("disabled", "true");
+ deleteTemplate.setAttribute("disabled", "true");
+ deleteTemplate.hidden = false;
+ resetTemplate.hidden = true;
+ } else {
+ header.value = name;
+ if (!addon.api.template.SYSTEM_TEMPLATE_NAMES.includes(name)) {
+ header.removeAttribute("disabled");
+ deleteTemplate.hidden = false;
+ resetTemplate.hidden = true;
+ } else {
+ header.setAttribute("disabled", "true");
+ deleteTemplate.setAttribute("disabled", "true");
+ deleteTemplate.hidden = true;
+ resetTemplate.hidden = false;
+ }
+ text.value = templateText;
+ text.removeAttribute("disabled");
+ saveTemplate.removeAttribute("disabled");
+ deleteTemplate.removeAttribute("disabled");
+ }
+}
+
+async function updatePreview() {
+ const name = getSelectedTemplateName();
+ const html = await addon.api.template.renderTemplatePreview(name);
+ const win = addon.data.templateEditor.window;
+ const container = win?.document.getElementById("preview-container");
+ if (container) {
+ container.innerHTML = "";
+ container.appendChild(
+ ztoolkit.getDOMParser().parseFromString(html, "text/html").body
+ );
+ }
+}
+
+function getSelectedTemplateName() {
+ const selectedTemplate = addon.data.templateEditor.templates.find((v, i) =>
+ addon.data.templateEditor.tableHelper?.treeInstance.selection.isSelected(i)
+ );
+ return selectedTemplate?.name || "";
+}
+
+function createTemplate() {
+ const template: NoteTemplate = {
+ name: `New Template: ${new Date().getTime()}`,
+ text: "",
+ };
+ addon.api.template.setTemplate(template);
+ refresh();
+}
+
+async function importNoteTemplate() {
+ const ids = await itemPicker();
+ const note: Zotero.Item = Zotero.Items.get(ids).filter((item: Zotero.Item) =>
+ item.isNote()
+ )[0];
+ if (!note) {
+ return;
+ }
+ const template: NoteTemplate = {
+ name: `Template from ${note.getNoteTitle()}: ${new Date().getTime()}`,
+ text: addon.api.sync.getNoteStatus(note.id)?.content || "",
+ };
+ addon.api.template.setTemplate(template);
+ refresh();
+}
+
+function saveSelectedTemplate() {
+ const name = getSelectedTemplateName();
+ const header = addon.data.templateEditor.window?.document.getElementById(
+ "editor-name"
+ ) as HTMLInputElement;
+ const text = addon.data.templateEditor.window?.document.getElementById(
+ "editor-textbox"
+ ) as HTMLTextAreaElement;
+
+ if (
+ addon.api.template.SYSTEM_TEMPLATE_NAMES.includes(name) &&
+ header.value !== name
+ ) {
+ showHint(
+ `Template ${name} is a system template. Modifying template name is not allowed.`
+ );
+ return;
+ }
+
+ const template = {
+ name: header.value,
+ text: text.value,
+ };
+ addon.api.template.setTemplate(template);
+ if (name !== template.name) {
+ addon.api.template.removeTemplate(name);
+ }
+ showHint(`Template ${template.name} saved.`);
+ const selectedId =
+ addon.data.templateEditor.tableHelper?.treeInstance.selection.selected
+ .values()
+ .next().value;
+ refresh().then(() => updateTable(selectedId));
+}
+
+function deleteSelectedTemplate() {
+ const name = getSelectedTemplateName();
+ if (addon.api.template.SYSTEM_TEMPLATE_NAMES.includes(name)) {
+ showHint(
+ `Template ${name} is a system template. Removing system template is note allowed.`
+ );
+ return;
+ }
+ addon.api.template.removeTemplate(name);
+ refresh();
+}
+
+function resetSelectedTemplate() {
+ const name = getSelectedTemplateName();
+ if (addon.api.template.SYSTEM_TEMPLATE_NAMES.includes(name)) {
+ const text = addon.data.templateEditor.window?.document.getElementById(
+ "editor-textbox"
+ ) as HTMLTextAreaElement;
+ text.value =
+ addon.api.template.DEFAULT_TEMPLATES.find((t) => t.name === name)?.text ||
+ "";
+ showHint(`Template ${name} is reset. Please save before leaving.`);
+ }
+}
diff --git a/src/modules/template/picker.ts b/src/modules/template/picker.ts
new file mode 100644
index 0000000..6ec573b
--- /dev/null
+++ b/src/modules/template/picker.ts
@@ -0,0 +1,64 @@
+import { Prompt } from "zotero-plugin-toolkit/dist/managers/prompt";
+import ToolkitGlobal from "zotero-plugin-toolkit/dist/managers/toolkitGlobal";
+import { addLineToNote } from "../../utils/note";
+
+export { updateTemplatePicker, showTemplatePicker };
+
+function showTemplatePicker(noteId?: number, lineIndex?: number) {
+ if (addon.data.prompt) {
+ addon.data.templatePicker = { noteId, lineIndex };
+ addon.data.prompt.promptNode.style.display = "flex";
+ addon.data.prompt.showCommands(
+ addon.data.prompt.commands.filter(
+ (cmd) => cmd.label === "BNotes Template"
+ )
+ );
+ }
+}
+
+function updateTemplatePicker() {
+ ztoolkit.Prompt.unregisterAll();
+ const templates = addon.api.template.getTemplateKeys();
+ ztoolkit.Prompt.register(
+ templates
+ .filter(
+ (template) =>
+ !addon.api.template.SYSTEM_TEMPLATE_NAMES.includes(template.name)
+ )
+ .map((template) => {
+ return {
+ name: `Template: ${template.name}`,
+ label: "BNotes Template",
+ callback: getTemplatePromptHandler(template.name),
+ };
+ })
+ );
+ if (!addon.data.prompt) {
+ addon.data.prompt = ToolkitGlobal.getInstance().prompt.instance;
+ }
+}
+
+function getTemplatePromptHandler(name: string) {
+ return async (prompt: Prompt) => {
+ ztoolkit.log(prompt, name);
+ prompt.promptNode.style.display = "none";
+ // TODO: add preview when command is selected
+ const targetNoteItem = Zotero.Items.get(
+ addon.data.templatePicker.noteId || addon.data.workspace.mainId
+ );
+ let html = "";
+ if (name.startsWith("[Item]")) {
+ html = await addon.api.template.runItemTemplate(name, {
+ targetNoteId: targetNoteItem.id,
+ });
+ } else {
+ html = await addon.api.template.runTemplate(name, "", []);
+ }
+ await addLineToNote(
+ targetNoteItem,
+ html,
+ addon.data.templatePicker.lineIndex
+ );
+ addon.data.templatePicker = {};
+ };
+}
diff --git a/src/modules/template/preview.ts b/src/modules/template/preview.ts
new file mode 100644
index 0000000..4046d9e
--- /dev/null
+++ b/src/modules/template/preview.ts
@@ -0,0 +1,120 @@
+import YAML = require("yamljs");
+import { getNoteLink } from "../../utils/link";
+
+export { renderTemplatePreview };
+
+async function renderTemplatePreview(
+ templateName: string,
+ inputItems?: Zotero.Item[]
+): Promise {
+ let html: string = "Preview rendering failed
";
+ if (inputItems) {
+ inputItems = ZoteroPane.getSelectedItems();
+ }
+ if (templateName.startsWith("[Text]")) {
+ html = await addon.api.template.runTemplate(templateName, "", []);
+ } else if (templateName.startsWith("[Item]")) {
+ const data = inputItems?.map((item) => item.id);
+ html = await addon.api.template.runItemTemplate(templateName, {
+ itemIds: data,
+ dryRun: true,
+ });
+ } else if (templateName.includes("ExportMDFileName")) {
+ // noteItem
+ const data = inputItems?.find((item) => item.isNote());
+ if (!data) {
+ html = "No note item selected
";
+ } else {
+ html = await addon.api.template.runTemplate(
+ templateName,
+ "noteItem",
+ [data],
+ {
+ dryRun: true,
+ }
+ );
+ }
+ } else if (templateName.includes("ExportMDFileHeader")) {
+ // noteItem
+ const data = inputItems?.find((item) => item.isNote());
+ if (!data) {
+ html = "No note item selected
";
+ } else {
+ const raw = await addon.api.template.runTemplate(
+ templateName,
+ "noteItem",
+ [data],
+ {
+ dryRun: true,
+ }
+ );
+ const header = Object.assign({}, JSON.parse(raw), {
+ version: data.version,
+ libraryID: data.libraryID,
+ itemKey: data.key,
+ });
+ html = `${YAML.stringify(header, 10)} `;
+ }
+ } else if (templateName.includes("QuickInsert")) {
+ // link, linkText, subNoteItem, noteItem
+ const data = inputItems?.find((item) => item.isNote());
+ if (!data) {
+ html = "No note item selected
";
+ } else {
+ const link = getNoteLink(data);
+ const linkText = data.getNoteTitle().trim() || link;
+ const subNoteItem = data;
+ const noteItem = Zotero.Items.get(addon.data.workspace.mainId);
+ html = await addon.api.template.runTemplate(
+ templateName,
+ "link, linkText, subNoteItem, noteItem",
+ [link, linkText, subNoteItem, noteItem],
+ {
+ dryRun: true,
+ }
+ );
+ }
+ } else if (templateName.includes("QuickBackLink")) {
+ // link, linkText, subNoteItem, noteItem
+ const data = inputItems?.find((item) => item.isNote());
+ if (!data) {
+ html = "No note item selected
";
+ } else {
+ const link = getNoteLink(data);
+ const noteItem = Zotero.Items.get(addon.data.workspace.mainId);
+ const linkText = noteItem.getNoteTitle().trim() || "Workspace Note";
+ const subNoteItem = data;
+ html = await addon.api.template.runTemplate(
+ templateName,
+ "link, linkText, subNoteItem, noteItem",
+ [link, linkText, subNoteItem, noteItem],
+ {
+ dryRun: true,
+ }
+ );
+ }
+ } else if (templateName.includes("QuickImport")) {
+ // link, noteItem
+ const data = inputItems?.find((item) => item.isNote());
+ if (!data) {
+ html = "No note item selected
";
+ } else {
+ const link = getNoteLink(data);
+ const noteItem = Zotero.Items.get(addon.data.workspace.mainId);
+ html = await addon.api.template.runTemplate(
+ templateName,
+ "link, noteItem",
+ [link, noteItem],
+ {
+ dryRun: true,
+ }
+ );
+ }
+ } else if (templateName.includes("QuickNote")) {
+ // annotationItem, topItem, noteItem
+ html = "Preview not available for QuickNote
";
+ } else {
+ html = `Preview not available for template ${templateName}
`;
+ }
+ return html;
+}
diff --git a/src/modules/userGuide.ts b/src/modules/userGuide.ts
new file mode 100644
index 0000000..e69de29
diff --git a/src/modules/workspace/content.ts b/src/modules/workspace/content.ts
new file mode 100644
index 0000000..8d36f16
--- /dev/null
+++ b/src/modules/workspace/content.ts
@@ -0,0 +1,448 @@
+import { config } from "../../../package.json";
+import { ICONS } from "../../utils/config";
+import { scroll } from "../../utils/editor";
+import { getString } from "../../utils/locale";
+import { getNoteTreeFlattened } from "../../utils/note";
+import { getPref } from "../../utils/prefs";
+import { waitUtilAsync } from "../../utils/wait";
+import { OutlineType } from "../../utils/workspace";
+import { saveFreeMind as _saveFreeMind } from "../export/freemind";
+
+function makeId(key: string) {
+ return `betternotes-workspace-${key}`;
+}
+
+export function initWorkspace(container: XUL.Box | undefined) {
+ if (!container) {
+ return;
+ }
+ function makeTooltipProp(
+ id: string,
+ content: string,
+ title: string,
+ callback: (ev: Event) => void
+ ) {
+ return {
+ tag: "div",
+ id,
+ classList: ["tooltip"],
+ children: [
+ {
+ tag: "button",
+ namespace: "html",
+ classList: ["tool-button"],
+ properties: {
+ innerHTML: content,
+ },
+ listeners: [
+ {
+ type: "click",
+ listener: callback,
+ },
+ ],
+ },
+ {
+ tag: "span",
+ classList: ["tooltiptext"],
+ properties: {
+ innerHTML: title,
+ },
+ },
+ ],
+ };
+ }
+ ztoolkit.UI.appendElement(
+ {
+ tag: "hbox",
+ id: makeId("top-container"),
+ styles: { width: "100%" },
+ properties: {},
+ attributes: {
+ flex: "1",
+ },
+ children: [
+ {
+ tag: "vbox",
+ id: makeId("outline-container"),
+ styles: {
+ overflow: "hidden",
+ display: "flex",
+ flexDirection: "column",
+ },
+ attributes: {
+ width: "330",
+ minwidth: "300",
+ flex: "1",
+ },
+ children: [
+ {
+ tag: "link",
+ properties: {
+ rel: "stylesheet",
+ href: `chrome://${config.addonRef}/content/tooltip.css`,
+ },
+ },
+ {
+ tag: "div",
+ id: makeId("outline-content"),
+ styles: {
+ height: "100%",
+ },
+ children: [
+ {
+ tag: "iframe",
+ id: makeId("outline-iframe"),
+ properties: {
+ width: "100%",
+ },
+ styles: {
+ border: "0",
+ height: "100%",
+ },
+ },
+ ],
+ },
+ {
+ tag: "div",
+ id: makeId("outline-buttons"),
+ styles: {
+ height: "50px",
+ display: "flex",
+ flexDirection: "row",
+ justifyContent: "space-between",
+ margin: "10px 20px 10px 20px",
+ },
+ children: [
+ makeTooltipProp(
+ makeId("switchOutline"),
+ ICONS.switchOutline,
+ getString("workspace.switchOutline"),
+ (ev) => {
+ setOutline(container);
+ }
+ ),
+ makeTooltipProp(
+ makeId("saveOutlineImage"),
+ ICONS.saveOutlineImage,
+ getString("workspace.saveOutlineImage"),
+ (ev) => {
+ saveImage(container);
+ }
+ ),
+ makeTooltipProp(
+ makeId("saveOutlineFreeMind"),
+ ICONS.saveOutlineFreeMind,
+ getString("workspace.saveOutlineFreeMind"),
+ (ev) => {
+ saveFreeMind();
+ }
+ ),
+ ],
+ },
+ ],
+ },
+ {
+ tag: "splitter",
+ id: makeId("outline-splitter"),
+ attributes: { collapse: "before" },
+ children: [
+ {
+ tag: "grippy",
+ },
+ ],
+ },
+ {
+ tag: "vbox",
+ id: makeId("editor-main-container"),
+ attributes: {
+ flex: "1",
+ width: "700",
+ },
+ styles: ztoolkit.isZotero7()
+ ? {}
+ : {
+ display: "flex",
+ flexDirection: "column",
+ },
+ children: ztoolkit.isZotero7()
+ ? []
+ : [
+ {
+ tag: "zoteronoteeditor",
+ namespace: "html",
+ id: makeId("editor-main"),
+ attributes: { flex: "1" },
+ styles: {
+ display: "flex",
+ height: "100%",
+ },
+ },
+ ],
+ },
+ {
+ tag: "splitter",
+ id: makeId("preview-splitter"),
+ attributes: { collapse: "after", state: "collapsed" },
+ children: [
+ {
+ tag: "grippy",
+ },
+ ],
+ },
+ {
+ tag: "vbox",
+ id: makeId("editor-preview-container"),
+ attributes: {
+ flex: "1",
+ width: "500",
+ },
+ styles: ztoolkit.isZotero7()
+ ? {}
+ : {
+ display: "flex",
+ flexDirection: "column",
+ },
+ children: ztoolkit.isZotero7()
+ ? []
+ : [
+ {
+ tag: "zoteronoteeditor",
+ namespace: "html",
+ id: makeId("editor-preview"),
+ attributes: { flex: "1" },
+ styles: {
+ display: "flex",
+ height: "100%",
+ },
+ },
+ ],
+ },
+ ],
+ },
+ container
+ );
+ // Manually add custom editor items in Zotero 7
+ if (ztoolkit.isZotero7()) {
+ const customElements = ztoolkit.getGlobal("customElements");
+ const mainEditorContainer = container.querySelector(
+ `#${makeId("editor-main-container")}`
+ );
+ const previewEditorContainer = container.querySelector(
+ `#${makeId("editor-preview-container")}`
+ );
+ const mainEditor = new (customElements.get("note-editor")!)();
+ mainEditor.id = makeId("editor-main");
+ mainEditor.setAttribute("flex", "1");
+ const previewEditor = new (customElements.get("note-editor")!)();
+ previewEditor.id = makeId("editor-preview");
+ previewEditor.setAttribute("flex", "1");
+ mainEditorContainer?.append(mainEditor);
+ previewEditorContainer?.append(previewEditor);
+ }
+
+ const outlineContainer = container.querySelector(
+ `#${makeId("outline-container")}`
+ ) as XUL.Box;
+ const outlineMut = new (ztoolkit.getGlobal("MutationObserver"))(
+ (mutations) => {
+ if (outlineContainer.getAttribute("collapsed") === "true") {
+ outlineContainer.style.removeProperty("display");
+ } else {
+ outlineContainer.style.display = "flex";
+ }
+ }
+ );
+ outlineMut.observe(outlineContainer, {
+ attributes: true,
+ attributeFilter: ["collapsed"],
+ });
+
+ setOutline(container, OutlineType.treeView);
+ initWorkspaceEditor(container, "main", addon.data.workspace.mainId);
+}
+
+export async function initWorkspaceEditor(
+ container: XUL.Box | undefined,
+ type: "main" | "preview",
+ noteId: number,
+ options: {
+ lineIndex?: number;
+ } = {}
+) {
+ const noteItem = Zotero.Items.get(noteId);
+ if (!noteItem.isNote()) {
+ throw new Error("initNoteEditor: not a note item");
+ }
+ const editorElem = container?.querySelector(
+ `#${makeId("editor-" + type)}`
+ ) as EditorElement;
+ await waitUtilAsync(() => Boolean(editorElem._initialized))
+ .then(() => ztoolkit.log("ok"))
+ .catch(() => ztoolkit.log("fail"));
+ if (!editorElem._initialized) {
+ throw new Error("initNoteEditor: waiting initialization failed");
+ }
+ editorElem.mode = "edit";
+ editorElem.viewMode = "library";
+ editorElem.parent = undefined;
+ editorElem.item = noteItem;
+ await waitUtilAsync(() => Boolean(editorElem._editorInstance));
+ await editorElem._editorInstance._initPromise;
+ if (typeof options.lineIndex === "number") {
+ scroll(editorElem._editorInstance, options.lineIndex);
+ }
+ return;
+}
+
+function getContainerType(
+ container: XUL.Box | undefined
+): "tab" | "window" | "unknown" {
+ if (!container) {
+ return "unknown";
+ }
+ return (container.getAttribute("workspace-type") || "unknown") as
+ | "tab"
+ | "window"
+ | "unknown";
+}
+
+export function toggleOutlinePane(
+ container: XUL.Box | undefined,
+ state?: "open" | "collapsed"
+) {
+ const splitter = container?.querySelector(`#${makeId("outline-splitter")}`);
+ if (!state) {
+ state =
+ splitter?.getAttribute("state") === "collapsed" ? "open" : "collapsed";
+ }
+ splitter?.setAttribute("state", state);
+}
+
+export function togglePreviewPane(
+ container: XUL.Box | undefined,
+ state?: "open" | "collapsed"
+) {
+ const splitter = container?.querySelector(`#${makeId("preview-splitter")}`);
+ if (!state) {
+ state =
+ splitter?.getAttribute("state") === "collapsed" ? "open" : "collapsed";
+ }
+ splitter?.setAttribute("state", state);
+}
+
+export function toggleNotesPane(state?: "open" | "collapsed") {
+ const splitter = document?.querySelector("#zotero-context-splitter");
+ if (!state) {
+ state =
+ splitter?.getAttribute("state") === "collapsed" ? "open" : "collapsed";
+ }
+ splitter?.setAttribute("state", state);
+}
+
+export function getWorkspaceEditor(
+ workspaceType: "tab" | "window",
+ editorType: "main" | "preview" = "main"
+) {
+ const container =
+ workspaceType === "tab"
+ ? addon.data.workspace.tab.container
+ : addon.data.workspace.window.container;
+ return (
+ container?.querySelector(`#${makeId(`editor-${editorType}`)}`) as
+ | EditorElement
+ | undefined
+ )?._editorInstance;
+}
+
+const SRC_LIST = [
+ "",
+ `chrome://${config.addonRef}/content/treeView.html`,
+ `chrome://${config.addonRef}/content/mindMap.html`,
+ `chrome://${config.addonRef}/content/bubbleMap.html`,
+];
+
+function setOutline(
+ container: XUL.Box,
+ newType: OutlineType = OutlineType.empty
+) {
+ if (newType === OutlineType.empty) {
+ newType = addon.data.workspace.outline + 1;
+ }
+ if (newType > OutlineType.bubbleMap) {
+ newType = OutlineType.treeView;
+ }
+ addon.data.workspace.outline = newType;
+ (
+ container.querySelector(`#${makeId("saveOutlineImage")}`) as HTMLDivElement
+ ).hidden = newType === OutlineType.treeView;
+ (
+ container.querySelector(
+ `#${makeId("saveOutlineFreeMind")}`
+ ) as HTMLDivElement
+ ).hidden = newType === OutlineType.treeView;
+ const iframe = container.querySelector(
+ `#${makeId("outline-iframe")}`
+ ) as HTMLIFrameElement;
+ iframe.setAttribute("src", SRC_LIST[addon.data.workspace.outline]);
+ updateOutline(container);
+ updateOutlineButtons(container);
+}
+
+export async function updateOutline(container: XUL.Box) {
+ const iframe = container.querySelector(
+ `#${makeId("outline-iframe")}`
+ ) as HTMLIFrameElement;
+ await waitUtilAsync(
+ () => iframe.contentWindow?.document.readyState === "complete"
+ );
+ iframe.contentWindow?.postMessage(
+ {
+ type: "setMindMapData",
+ nodes: getNoteTreeFlattened(
+ Zotero.Items.get(addon.data.workspace.mainId),
+ { keepLink: true }
+ ),
+ workspaceType: getContainerType(container),
+ expandLevel: getPref("workspace.outline.expandLevel"),
+ },
+ "*"
+ );
+}
+
+function updateOutlineButtons(container: XUL.Box) {
+ const outlineType = addon.data.workspace.outline;
+ const isTreeView = outlineType === OutlineType.treeView;
+ (
+ container.querySelector(`#${makeId("saveOutlineImage")}`) as HTMLDivElement
+ ).style.visibility = isTreeView ? "hidden" : "visible";
+ (
+ container.querySelector(
+ `#${makeId("saveOutlineFreeMind")}`
+ ) as HTMLDivElement
+ ).style.visibility = isTreeView ? "hidden" : "visible";
+}
+
+function saveImage(container: XUL.Box) {
+ const iframe = container.querySelector(
+ `#${makeId("outline-iframe")}`
+ ) as HTMLIFrameElement;
+ iframe.contentWindow?.postMessage(
+ {
+ type: "saveSVG",
+ },
+ "*"
+ );
+}
+
+async function saveFreeMind() {
+ // TODO: uncouple this part
+ const filename = await new ztoolkit.FilePicker(
+ `${Zotero.getString("fileInterface.export")} FreeMind XML`,
+ "save",
+ [["FreeMind XML File(*.mm)", "*.mm"]],
+ `${Zotero.Items.get(addon.data.workspace.mainId).getNoteTitle()}.mm`
+ ).open();
+ if (filename) {
+ await _saveFreeMind(filename, addon.data.workspace.mainId);
+ }
+}
diff --git a/src/modules/workspace/message.ts b/src/modules/workspace/message.ts
new file mode 100644
index 0000000..b2d382f
--- /dev/null
+++ b/src/modules/workspace/message.ts
@@ -0,0 +1,85 @@
+import {
+ getEditorInstance,
+ moveHeading,
+ scroll,
+ updateHeadingTextAtLine,
+} from "../../utils/editor";
+import { showHintWithLink } from "../../utils/hint";
+import { getNoteLinkParams } from "../../utils/link";
+import { getNoteTree, getNoteTreeNodeById } from "../../utils/note";
+import { formatPath } from "../../utils/str";
+
+export async function messageHandler(ev: MessageEvent) {
+ switch (ev.data.type) {
+ case "jumpNode": {
+ const editor = addon.api.workspace.getWorkspaceEditor(
+ ev.data.workspaceType,
+ "main"
+ );
+ if (!editor) {
+ return;
+ }
+ scroll(editor, ev.data.lineIndex);
+ return;
+ }
+ case "openNote": {
+ const linkParams = getNoteLinkParams(ev.data.link);
+ if (!linkParams.noteItem) {
+ return;
+ }
+ addon.hooks.onOpenNote(linkParams.noteItem.id, "preview", {
+ lineIndex: linkParams.lineIndex || undefined,
+ });
+ return;
+ }
+ case "moveNode": {
+ const noteItem = Zotero.Items.get(addon.data.workspace.mainId);
+ let tree = getNoteTree(noteItem);
+ let fromNode = getNoteTreeNodeById(noteItem, ev.data.fromID, tree);
+ let toNode = getNoteTreeNodeById(noteItem, ev.data.toID, tree);
+ moveHeading(
+ getEditorInstance(noteItem.id),
+ fromNode!,
+ toNode!,
+ ev.data.moveType
+ );
+ return;
+ }
+ case "editNode": {
+ const editor = addon.api.workspace.getWorkspaceEditor(
+ ev.data.workspaceType,
+ "main"
+ );
+ if (!editor) {
+ return;
+ }
+ updateHeadingTextAtLine(
+ editor,
+ ev.data.lineIndex,
+ ev.data.text.replace(/[\r\n]/g, "")
+ );
+ return;
+ }
+ case "saveSVGReturn": {
+ const filename = await new ztoolkit.FilePicker(
+ `${Zotero.getString("fileInterface.export")} SVG Image`,
+ "save",
+ [["SVG File(*.svg)", "*.svg"]],
+ `${Zotero.Items.get(addon.data.workspace.mainId).getNoteTitle()}.svg`
+ ).open();
+ if (filename) {
+ await Zotero.File.putContentsAsync(formatPath(filename), ev.data.image);
+ showHintWithLink(
+ `Image Saved to ${filename}`,
+ "Show in Folder",
+ (ev) => {
+ Zotero.File.reveal(filename);
+ }
+ );
+ }
+ return;
+ }
+ default:
+ return;
+ }
+}
diff --git a/src/modules/workspace/tab.ts b/src/modules/workspace/tab.ts
new file mode 100644
index 0000000..d7447ca
--- /dev/null
+++ b/src/modules/workspace/tab.ts
@@ -0,0 +1,314 @@
+import { config } from "../../../package.json";
+import { ICONS } from "../../utils/config";
+import { showHint } from "../../utils/hint";
+import { getString } from "../../utils/locale";
+import { getPref, setPref } from "../../utils/prefs";
+import { waitUtilAsync } from "../../utils/wait";
+// TODO: uncouple these imports
+import {} from "./content";
+import { messageHandler } from "./message";
+
+export const TAB_TYPE = "betternotes";
+
+export function registerWorkspaceTab() {
+ const tabContainer = document.querySelector("#tab-bar-container");
+ if (!tabContainer) {
+ return;
+ }
+ tabContainer.removeAttribute("hidden");
+ const mut = new (ztoolkit.getGlobal("MutationObserver"))((muts) => {
+ tabContainer.removeAttribute("hidden");
+ });
+ mut.observe(tabContainer, {
+ attributes: true,
+ attributeFilter: ["hidden"],
+ });
+ waitUtilAsync(() =>
+ Boolean(ztoolkit.getGlobal("ZoteroContextPane")._notifierID)
+ ).then(() => {
+ addWorkspaceTab().then(() => {
+ restoreWorkspaceTab();
+ });
+ });
+ window.addEventListener("message", (e) => messageHandler(e), false);
+}
+
+async function addWorkspaceTab() {
+ let { id, container } = Zotero_Tabs.add({
+ type: TAB_TYPE,
+ title: getString("tab.name"),
+ index: 1,
+ data: {
+ itemID: addon.data.workspace.mainId,
+ },
+ select: false,
+ onClose: () => {
+ setWorkspaceTabStatus(false);
+ addWorkspaceTab();
+ },
+ });
+ await waitUtilAsync(() =>
+ Boolean(document.querySelector(`.tabs-wrapper .tab[data-id=${id}]`))
+ );
+ const tabElem = document.querySelector(
+ `.tabs-wrapper .tab[data-id=${id}]`
+ ) as HTMLDivElement;
+ tabElem.style.width = "30px";
+ tabElem.style.minWidth = "30px";
+ tabElem.style.maxWidth = "30px";
+ tabElem.style.padding = "0px";
+ const content = tabElem.querySelector(".tab-name") as HTMLDivElement;
+ const close = tabElem.querySelector(".tab-close") as HTMLDivElement;
+ content.style.verticalAlign = "middle";
+ content.style.width = "20px";
+ content.style.height = "20px";
+ content.style.display = "inline";
+ content.innerHTML = "";
+ ztoolkit.UI.appendElement(
+ {
+ tag: "span",
+ classList: ["icon-bg"],
+ styles: {
+ backgroundImage: `url("chrome://${config.addonRef}/content/icons/favicon.png")`,
+ },
+ },
+ content
+ );
+ close.style.visibility = "hidden";
+ addon.data.workspace.tab.id = id;
+ container.setAttribute("workspace-type", "tab");
+ addon.data.workspace.tab.container = container;
+}
+
+function hoverWorkspaceTab(hovered: boolean) {
+ Array.from(document.querySelectorAll(".tab-toggle")).forEach((elem) => {
+ (elem as HTMLDivElement).style.visibility = hovered ? "visible" : "hidden";
+ });
+ const tabElem = document.querySelector(
+ `.tabs-wrapper .tab[data-id=${addon.data.workspace.tab.id}]`
+ ) as HTMLDivElement;
+ const content = tabElem.querySelector(".tab-name") as HTMLDivElement;
+ content.removeAttribute("style");
+ if (hovered) {
+ if (ztoolkit.isZotero7()) {
+ content.style["-moz-box-pack" as any] = "start";
+ } else {
+ content.style.position = "absolute";
+ content.style.left = "22px";
+ }
+ }
+}
+
+function updateWorkspaceTabToggleButton(
+ type: "outline" | "preview" | "notes",
+ state: "open" | "collapsed"
+) {
+ const elem = document.querySelector(
+ `#betternotes-tab-toggle-${type}`
+ ) as HTMLDivElement;
+ if (!elem) {
+ return;
+ }
+ if (state !== "collapsed") {
+ state = "open";
+ }
+ elem.innerHTML = ICONS[`workspace_${type}_${state}`];
+}
+
+function registerWorkspaceTabPaneObserver() {
+ const outlineSplitter = document.querySelector(
+ "#betternotes-workspace-outline-splitter"
+ );
+ const outlineMut = new (ztoolkit.getGlobal("MutationObserver"))((muts) => {
+ updateWorkspaceTabToggleButton(
+ "outline",
+ outlineSplitter!.getAttribute("state")! as "open" | "collapsed"
+ );
+ });
+ outlineMut.observe(outlineSplitter!, {
+ attributes: true,
+ attributeFilter: ["state"],
+ });
+ const previewSplitter = document.querySelector(
+ "#betternotes-workspace-preview-splitter"
+ );
+ const previeweMut = new (ztoolkit.getGlobal("MutationObserver"))((muts) => {
+ updateWorkspaceTabToggleButton(
+ "preview",
+ previewSplitter!.getAttribute("state")! as "open" | "collapsed"
+ );
+ });
+ previeweMut.observe(previewSplitter!, {
+ attributes: true,
+ attributeFilter: ["state"],
+ });
+ const notesSplitter = document.querySelector("#zotero-context-splitter");
+ const notesMut = new (ztoolkit.getGlobal("MutationObserver"))((muts) => {
+ updateWorkspaceTabToggleButton(
+ "notes",
+ notesSplitter!.getAttribute("state")! as "open" | "collapsed"
+ );
+ });
+ notesMut.observe(notesSplitter!, {
+ attributes: true,
+ attributeFilter: ["state"],
+ });
+}
+
+function isContextPaneInitialized() {
+ return (
+ (document.querySelector(".notes-pane-deck")?.childElementCount || 0) > 0
+ );
+}
+
+export async function activateWorkspaceTab() {
+ if (Zotero_Tabs.selectedType === TAB_TYPE && isContextPaneInitialized()) {
+ (
+ document.querySelector("#zotero-tab-toolbar") as XUL.Box
+ ).style.visibility = "collapse";
+ const toolbar = document.querySelector(
+ "#zotero-context-toolbar-extension"
+ ) as XUL.Box;
+ toolbar.style.visibility = "collapse";
+ toolbar.nextElementSibling?.setAttribute("selectedIndex", "1");
+ }
+
+ if (addon.data.workspace.tab.active) {
+ ztoolkit.log("workspace tab is already active");
+ return;
+ }
+ setWorkspaceTabStatus(true);
+ // reset tab style
+ await waitUtilAsync(() =>
+ Boolean(
+ document.querySelector(
+ `.tabs-wrapper .tab[data-id=${addon.data.workspace.tab.id}]`
+ )
+ )
+ );
+ const tabElem = document.querySelector(
+ `.tabs-wrapper .tab[data-id=${addon.data.workspace.tab.id}]`
+ ) as HTMLDivElement;
+ tabElem.removeAttribute("style");
+ const content = tabElem.querySelector(".tab-name") as HTMLDivElement;
+ const close = tabElem.querySelector(".tab-close") as HTMLDivElement;
+ content.removeAttribute("style");
+ content.append(document.createTextNode(getString("tab.name")));
+ close.style.removeProperty("visibility");
+ ztoolkit.UI.insertElementBefore(
+ {
+ tag: "fragment",
+ children: [
+ {
+ tag: "div",
+ id: "betternotes-tab-toggle-outline",
+ classList: ["tab-close", "tab-toggle"],
+ styles: {
+ right: "56px",
+ },
+ properties: {
+ innerHTML: ICONS.workspace_outline_open,
+ },
+ listeners: [
+ {
+ type: "click",
+ listener: (ev) => {
+ addon.api.workspace.toggleOutlinePane(
+ addon.data.workspace.tab.container
+ );
+ },
+ },
+ ],
+ },
+ {
+ tag: "div",
+ id: "betternotes-tab-toggle-preview",
+ classList: ["tab-close", "tab-toggle"],
+ styles: {
+ right: "40px",
+ },
+ properties: {
+ innerHTML: ICONS.workspace_preview_collapsed,
+ },
+ listeners: [
+ {
+ type: "click",
+ listener: (ev) => {
+ addon.api.workspace.togglePreviewPane(
+ addon.data.workspace.tab.container
+ );
+ },
+ },
+ ],
+ },
+ {
+ tag: "div",
+ id: "betternotes-tab-toggle-notes",
+ classList: ["tab-close", "tab-toggle"],
+ styles: {
+ right: "24px",
+ },
+ properties: {
+ innerHTML:
+ document
+ .querySelector("#zotero-context-splitter")
+ ?.getAttribute("state") === "open"
+ ? ICONS.workspace_notes_open
+ : ICONS.workspace_notes_collapsed,
+ },
+ listeners: [
+ {
+ type: "click",
+ listener: (ev) => {
+ if (isContextPaneInitialized()) {
+ addon.api.workspace.toggleNotesPane();
+ return;
+ }
+ showHint(getString("workspace.notesPane.hint"));
+ },
+ },
+ ],
+ },
+ ],
+ },
+ close
+ );
+ hoverWorkspaceTab(false);
+ tabElem.addEventListener("mouseenter", () => {
+ if (Zotero_Tabs.selectedType !== "betternotes") {
+ return;
+ }
+ hoverWorkspaceTab(true);
+ });
+ tabElem.addEventListener("mousedown", () => hoverWorkspaceTab(true));
+ tabElem.addEventListener("mouseleave", () => hoverWorkspaceTab(false));
+ // load workspace content
+ addon.api.workspace.initWorkspace(addon.data.workspace.tab.container);
+ registerWorkspaceTabPaneObserver();
+}
+
+export function deActivateWorkspaceTab() {
+ if (!isContextPaneInitialized()) {
+ return;
+ }
+ (
+ document.querySelector("#zotero-tab-toolbar") as XUL.Box
+ ).style.removeProperty("visibility");
+ const toolbar = document.querySelector(
+ "#zotero-context-toolbar-extension"
+ ) as XUL.Box;
+ toolbar.style.removeProperty("visibility");
+}
+
+function restoreWorkspaceTab() {
+ return;
+ if (1 || getPref("workspace.tab.active")) {
+ ztoolkit.log("restore workspace tab");
+ activateWorkspaceTab();
+ }
+}
+
+function setWorkspaceTabStatus(status: boolean) {
+ addon.data.workspace.tab.active = status;
+ setPref("workspace.tab.active", status);
+}
diff --git a/src/modules/workspace/window.ts b/src/modules/workspace/window.ts
new file mode 100644
index 0000000..3d578db
--- /dev/null
+++ b/src/modules/workspace/window.ts
@@ -0,0 +1,5 @@
+export function showWorkspaceWindow() {
+ // TODO
+ // const win;
+ // win.addEventListener("message", (e) => messageHandler(e), false);
+}
diff --git a/src/note/noteExportController.ts b/src/note/noteExportController.ts
deleted file mode 100644
index ecf74b2..0000000
--- a/src/note/noteExportController.ts
+++ /dev/null
@@ -1,453 +0,0 @@
-/*
- * This file realizes note export.
- */
-
-import BetterNotes from "../addon";
-import AddonBase from "../module";
-
-class NoteExport extends AddonBase {
- _exportPath: string;
- _exportFileInfo: Array<{
- link: string;
- id: number;
- note: Zotero.Item;
- filename: string;
- }>;
- _pdfPrintPromise: _ZoteroTypes.PromiseObject;
- _docxPromise: _ZoteroTypes.PromiseObject;
- _docxBlob: Blob;
-
- constructor(parent: BetterNotes) {
- super(parent);
- this._exportFileInfo = [];
- }
-
- async exportNote(
- note: Zotero.Item,
- options: {
- embedLink?: boolean;
- exportNote?: boolean;
- exportMD?: boolean;
- exportSubMD?: boolean;
- exportAutoSync?: boolean;
- exportYAMLHeader?: boolean;
- exportDocx?: boolean;
- exportPDF?: boolean;
- exportFreeMind?: boolean;
- } = {
- embedLink: true,
- exportNote: false,
- exportMD: true,
- exportSubMD: false,
- exportAutoSync: false,
- exportYAMLHeader: true,
- exportDocx: false,
- exportPDF: false,
- exportFreeMind: false,
- }
- ) {
- // Trick: options containing 'export' all false? return
- if (
- !Object.keys(options)
- .filter((k) => k.includes("export"))
- .find((k) => options[k])
- ) {
- this._Addon.toolkit.Tool.log("options containing 'export' all false");
- return;
- }
- this._exportFileInfo = [];
-
- let newNote: Zotero.Item;
- if (options.embedLink || options.exportNote) {
- const noteID = await ZoteroPane.newNote();
- newNote = Zotero.Items.get(noteID) as Zotero.Item;
- const rootNoteIds = [note.id];
-
- const convertResult = await this._Addon.NoteUtils.convertNoteLines(
- note,
- rootNoteIds,
- options.embedLink
- );
-
- await this._Addon.NoteUtils.setLinesToNote(newNote, convertResult.lines);
-
- await Zotero.DB.executeTransaction(async () => {
- await Zotero.Notes.copyEmbeddedImages(note, newNote);
- for (const subNote of convertResult.subNotes) {
- await Zotero.Notes.copyEmbeddedImages(subNote, newNote);
- }
- });
- } else {
- newNote = note;
- }
-
- if (options.exportMD) {
- const filename = await this._Addon.toolkit.Tool.openFilePicker(
- `${Zotero.getString("fileInterface.export")} MarkDown Document`,
- "save",
- [["MarkDown File(*.md)", "*.md"]],
- `${newNote.getNoteTitle()}.md`
- );
- if (filename) {
- this._exportPath = this._Addon.NoteUtils.formatPath(
- Zotero.File.pathToFile(filename).parent.path + "/attachments"
- );
- await this._exportMD(
- newNote,
- filename,
- false,
- options.exportYAMLHeader
- );
- }
- }
- if (options.exportDocx) {
- const instance: Zotero.EditorInstance =
- this._Addon.WorkspaceWindow.getEditorInstance(newNote);
- this._docxPromise = Zotero.Promise.defer();
- instance._iframeWindow.postMessage({ type: "exportDocx" }, "*");
- await this._docxPromise.promise;
- const filename = await this._Addon.toolkit.Tool.openFilePicker(
- `${Zotero.getString("fileInterface.export")} MS Word Document`,
- "save",
- [["MS Word Document(*.docx)", "*.docx"]],
- `${newNote.getNoteTitle()}.docx`
- );
- if (filename) {
- await this._exportDocx(filename);
- }
- }
- if (options.exportPDF) {
- this._Addon.toolkit.Tool.log(newNote);
- let _w: Window;
- let t = 0;
- ZoteroPane.selectItem(note.id);
- do {
- ZoteroPane.openNoteWindow(newNote.id);
- _w = ZoteroPane.findNoteWindow(newNote.id);
- await Zotero.Promise.delay(10);
- t += 1;
- } while (!_w && t < 500);
- ZoteroPane.selectItem(note.id);
- _w.resizeTo(900, 650);
- const editor: any = _w.document.querySelector("#zotero-note-editor");
- t = 0;
- while (
- !(
- editor.getCurrentInstance &&
- editor.getCurrentInstance() &&
- editor
- .getCurrentInstance()
- ._iframeWindow.document.body.getAttribute("betternotes-status") !==
- "initialized"
- ) &&
- t < 500
- ) {
- t += 1;
- await Zotero.Promise.delay(10);
- }
- const instance: Zotero.EditorInstance = editor.getCurrentInstance();
- instance._iframeWindow.document.querySelector("#bn-headings")?.remove();
- this._pdfPrintPromise = Zotero.Promise.defer();
- instance._iframeWindow.postMessage({ type: "exportPDF" }, "*");
- await this._pdfPrintPromise.promise;
- this._Addon.toolkit.Tool.log("print finish detected");
- const closeFlag = _w.confirm(
- "Printing finished. Do you want to close the preview window?"
- );
- if (closeFlag) {
- _w.close();
- }
- }
- if (options.exportFreeMind) {
- const filename = await this._Addon.toolkit.Tool.openFilePicker(
- `${Zotero.getString("fileInterface.export")} FreeMind`,
- "save",
- [["FreeMind(*.mm)", "*.mm"]],
- `${newNote.getNoteTitle()}.mm`
- );
- if (filename) {
- await this._exportFreeMind(newNote, filename);
- }
- }
- if (!options.exportNote) {
- if (newNote.id !== note.id) {
- const _w: Window = ZoteroPane.findNoteWindow(newNote.id);
- if (_w) {
- _w.close();
- }
- await Zotero.Items.erase(newNote.id);
- }
- } else {
- ZoteroPane.openNoteWindow(newNote.id);
- }
- }
-
- async exportNotesToMDFiles(
- notes: Zotero.Item[],
- options: {
- useEmbed?: boolean;
- useSync?: boolean;
- filedir?: string;
- withMeta?: boolean;
- } = {}
- ) {
- Components.utils.import("resource://gre/modules/osfile.jsm");
- this._exportFileInfo = [];
- let filedir =
- options.filedir ||
- (await this._Addon.toolkit.Tool.openFilePicker(
- Zotero.getString(
- options.useSync ? "sync.sync" : "fileInterface.export"
- ) + " MarkDown",
- "folder"
- ));
-
- filedir = Zotero.File.normalizeToUnix(filedir);
-
- if (!filedir) {
- this._Addon.toolkit.Tool.log("export, filepath invalid");
- return;
- }
-
- this._exportPath = this._Addon.NoteUtils.formatPath(
- OS.Path.join(filedir, "attachments")
- );
-
- notes = notes.filter((n) => n && n.getNote);
-
- if (options.useEmbed) {
- for (const note of notes) {
- let newNote: Zotero.Item;
- if (this._Addon.NoteParse.parseLinkInText(note.getNote())) {
- const noteID = await ZoteroPane.newNote();
- newNote = Zotero.Items.get(noteID) as Zotero.Item;
- const rootNoteIds = [note.id];
-
- const convertResult = await this._Addon.NoteUtils.convertNoteLines(
- note,
- rootNoteIds,
- true
- );
-
- await this._Addon.NoteUtils.setLinesToNote(
- newNote,
- convertResult.lines
- );
-
- await Zotero.DB.executeTransaction(async () => {
- await Zotero.Notes.copyEmbeddedImages(note, newNote);
- for (const subNote of convertResult.subNotes) {
- await Zotero.Notes.copyEmbeddedImages(subNote, newNote);
- }
- });
- } else {
- newNote = note;
- }
-
- let filename = OS.Path.join(filedir, await this._getFileName(note));
- filename = filename.replace(/\\/g, "/");
-
- await this._exportMD(
- newNote,
- filename,
- newNote.id !== note.id,
- options.withMeta
- );
- }
- } else {
- // Export every linked note as a markdown file
- // Find all linked notes that need to be exported
- const inputIds = notes.map((n) => n.id);
- let allNoteIds: number[] = notes.map((n) => n.id);
- for (const note of notes) {
- const linkMatches = note
- .getNote()
- .match(/zotero:\/\/note\/\w+\/\w+\//g);
- if (!linkMatches) {
- continue;
- }
- const subNoteIds = (
- await Promise.all(
- linkMatches.map(async (link) =>
- this._Addon.NoteUtils.getNoteFromLink(link)
- )
- )
- )
- .filter((res) => res.item)
- .map((res) => res.item.id);
- allNoteIds = allNoteIds.concat(subNoteIds);
- }
- allNoteIds = Array.from(new Set(allNoteIds));
- const allNoteItems: Zotero.Item[] = Zotero.Items.get(
- allNoteIds
- ) as Zotero.Item[];
- const noteLinkDict = [];
- for (const _note of allNoteItems) {
- noteLinkDict.push({
- link: this._Addon.NoteUtils.getNoteLink(_note),
- id: _note.id,
- note: _note,
- filename: await this._getFileName(_note, filedir),
- });
- }
- this._exportFileInfo = noteLinkDict;
-
- for (const noteInfo of noteLinkDict) {
- let exportPath = OS.Path.join(filedir, noteInfo.filename);
- if (
- options.useSync &&
- !inputIds.includes(noteInfo.id) &&
- (await OS.File.exists(exportPath))
- ) {
- // Avoid overwrite existing notes that are waiting to be synced.
- continue;
- }
- const content = await this._exportMD(
- noteInfo.note,
- exportPath,
- false,
- options.withMeta
- );
- if (options.useSync) {
- this._Addon.SyncController.updateNoteSyncStatus(noteInfo.note, {
- path: filedir,
- filename: noteInfo.filename,
- md5: Zotero.Utilities.Internal.md5(
- this._Addon.SyncUtils.getMDStatusFromContent(content).content,
- false
- ),
- noteMd5: Zotero.Utilities.Internal.md5(
- noteInfo.note.getNote(),
- false
- ),
- lastsync: new Date().getTime(),
- itemID: noteInfo.id,
- });
- }
- }
- }
- }
-
- private async _exportDocx(filename: string) {
- await Zotero.File.putContentsAsync(filename, this._docxBlob);
- const progress = this._Addon.ZoteroViews.showProgressWindow(
- "Better Notes",
- `Note Saved to ${filename}`
- );
- // Just a placeholder
- progress.addDescription('Open Folder ');
- (await this._Addon.ZoteroViews.getProgressDocument(progress))
- .querySelector("label[href]")
- .addEventListener("click", async (e) => {
- e.stopPropagation();
- e.preventDefault();
- await Zotero.File.reveal(filename);
- });
- progress.progress.setProgress(100);
- }
-
- private async _exportMD(
- note: Zotero.Item,
- filename: string,
- deleteAfterExport: boolean,
- withMeta: boolean
- ) {
- const hasImage = note.getNote().includes(" Open Folder');
- (await this._Addon.ZoteroViews.getProgressDocument(progress))
- .querySelector("label[href]")
- .addEventListener("click", async (e) => {
- e.stopPropagation();
- e.preventDefault();
- await Zotero.File.reveal(filename);
- });
- progress.progress.setProgress(100);
- if (deleteAfterExport) {
- const _w: Window = ZoteroPane.findNoteWindow(note.id);
- if (_w) {
- _w.close();
- }
- await Zotero.Items.erase(note.id);
- }
- return content;
- }
-
- private async _exportFreeMind(noteItem: Zotero.Item, filename: string) {
- filename = this._Addon.NoteUtils.formatPath(filename);
- await Zotero.File.putContentsAsync(
- filename,
- this._Addon.NoteParse.parseNoteToFreemind(noteItem)
- );
- const progress = this._Addon.ZoteroViews.showProgressWindow(
- "Better Notes",
- `Note Saved to ${filename}`
- );
- // Just a placeholder
- progress.addDescription('Open Folder ');
- (await this._Addon.ZoteroViews.getProgressDocument(progress))
- .querySelector("label[href]")
- .addEventListener("click", async (e) => {
- e.stopPropagation();
- e.preventDefault();
- await Zotero.File.reveal(filename);
- });
- progress.progress.setProgress(100);
- }
-
- private async _getFileName(
- noteItem: Zotero.Item,
- filedir: string = undefined
- ) {
- if (filedir !== undefined && (await OS.File.exists(filedir))) {
- const mdRegex = /\.(md|MD|Md|mD)$/;
- let matchedFileName = null;
- let matchedDate = new Date(0);
- await Zotero.File.iterateDirectory(
- filedir,
- 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 OS.File.stat(entry.path);
- if (stat.lastModificationDate > matchedDate) {
- matchedFileName = entry.name;
- matchedDate = stat.lastModificationDate;
- }
- }
- }
- }
- );
- if (matchedFileName) {
- return matchedFileName;
- }
- }
- return (
- (await this._Addon.TemplateController.renderTemplateAsync(
- "[ExportMDFileName]",
- "noteItem",
- [noteItem]
- )) as string
- ).replace(/\\/g, "-");
- }
-}
-
-export default NoteExport;
diff --git a/src/note/noteExportWindow.ts b/src/note/noteExportWindow.ts
deleted file mode 100644
index 09cf016..0000000
--- a/src/note/noteExportWindow.ts
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * This file contains note export window code.
- */
-
-import BetterNotes from "../addon";
-import AddonBase from "../module";
-
-class NoteExportWindow extends AddonBase {
- private io: {
- dataIn: any;
- dataOut: any;
- deferred?: typeof Promise;
- };
- private _window: Window;
- private options: string[];
-
- constructor(parent: BetterNotes) {
- super(parent);
- this.options = [
- "embedLink",
- "exportNote",
- "exportMD",
- "exportSubMD",
- "exportAutoSync",
- "exportYAMLHeader",
- "exportDocx",
- "exportPDF",
- "exportFreeMind",
- ];
- }
-
- getOption(optionKey: string) {
- return (this._window.document.getElementById(optionKey) as XUL.Checkbox)
- ?.checked;
- }
-
- setOption(optionKey: string, checked: boolean) {
- (this._window.document.getElementById(optionKey) as XUL.Checkbox).checked =
- checked;
- }
-
- setOptionDisabled(optionKey: string, disabled: boolean) {
- (this._window.document.getElementById(optionKey) as XUL.Checkbox).disabled =
- disabled;
- }
-
- doLoad(_window: Window) {
- this._window = _window;
-
- this.io = (this._window as unknown as XUL.XULWindow).arguments[0];
-
- const initOptions = (optionKey: string) => {
- let pref = Zotero.Prefs.get(`Knowledge4Zotero.${optionKey}`) as boolean;
- if (typeof pref !== "undefined") {
- this.setOption(optionKey, pref);
- }
- };
- this.options.forEach(initOptions);
- this.doUpdate();
- }
-
- doUpdate(event?: XUL.XULEvent) {
- if (event) {
- if (event.target.id === "embedLink" && this.getOption("embedLink")) {
- this.setOption("exportSubMD", false);
- this.setOption("exportAutoSync", false);
- }
- if (event.target.id === "exportSubMD") {
- if (this.getOption("exportSubMD")) {
- this.setOption("embedLink", false);
- } else {
- this.setOption("exportAutoSync", false);
- }
- }
- }
-
- this.setOptionDisabled(
- "exportSubMD",
- !this.getOption("exportMD") || this.getOption("embedLink")
- );
-
- this.setOptionDisabled(
- "exportAutoSync",
- !this.getOption("exportMD") || !this.getOption("exportSubMD")
- );
- }
-
- doUnload() {
- this.io.deferred && this.io.deferred.resolve();
- }
-
- doAccept() {
- this.io.dataOut = {};
- const saveOptions = (optionKey: string) => {
- const pref = this.getOption(optionKey);
- Zotero.Prefs.set(`Knowledge4Zotero.${optionKey}`, pref);
- this.io.dataOut[optionKey] = pref;
- };
- this.options.forEach(saveOptions);
-
- this._Addon.toolkit.Tool.log(this.io.dataOut);
- }
-}
-
-export default NoteExportWindow;
diff --git a/src/note/noteImportController.ts b/src/note/noteImportController.ts
deleted file mode 100644
index 4f2c8fb..0000000
--- a/src/note/noteImportController.ts
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * This file realizes md import.
- */
-
-import BetterNotes from "../addon";
-import AddonBase from "../module";
-
-class NoteImport extends AddonBase {
- constructor(parent: BetterNotes) {
- super(parent);
- }
-
- async doImport(
- noteItem: Zotero.Item = undefined,
- options: {
- ignoreVersion?: boolean;
- append?: boolean;
- } = {}
- ) {
- const filepath = await this._Addon.toolkit.Tool.openFilePicker(
- `${Zotero.getString("fileInterface.import")} MarkDown Document`,
- "open",
- [["MarkDown File(*.md)", "*.md"]]
- );
- if (filepath) {
- await this.importMDFileToNote(filepath, noteItem, options);
- }
- }
-
- async importMDFileToNote(
- file: string,
- noteItem: Zotero.Item = undefined,
- options: {
- ignoreVersion?: boolean;
- append?: boolean;
- } = {}
- ) {
- let mdStatus: MDStatus;
- try {
- mdStatus = await this._Addon.SyncUtils.getMDStatus(file);
- } catch (e) {
- this._Addon.toolkit.Tool.log(`Import: ${String(e)}`);
- }
- if (!options.ignoreVersion && mdStatus.meta?.version < noteItem?.version) {
- if (
- !confirm(
- `The target note seems to be newer than the file ${file}. Are you sure you want to import it anyway?`
- )
- ) {
- return;
- }
- }
- const noteStatus = noteItem
- ? this._Addon.SyncUtils.getNoteStatus(noteItem)
- : {
- meta: '',
- content: "",
- tail: "
",
- };
-
- if (!noteItem) {
- noteItem = new Zotero.Item("note");
- noteItem.libraryID = ZoteroPane.getSelectedLibraryID();
- if (ZoteroPane.getCollectionTreeRow().isCollection()) {
- noteItem.addToCollection(ZoteroPane.getCollectionTreeRow().ref.id);
- }
- await noteItem.saveTx({
- notifierData: {
- autoSyncDelay: Zotero.Notes.AUTO_SYNC_DELAY,
- },
- });
- }
- const parsedContent = await this._Addon.NoteParse.parseMDToNote(
- mdStatus,
- noteItem,
- true
- );
- this._Addon.toolkit.Tool.log("import", noteStatus);
-
- if (options.append) {
- await this._Addon.NoteUtils.addLineToNote(
- noteItem,
- parsedContent,
- Number.MAX_VALUE
- );
- } else {
- noteItem.setNote(noteStatus.meta + parsedContent + noteStatus.tail);
- await noteItem.saveTx({
- notifierData: {
- autoSyncDelay: Zotero.Notes.AUTO_SYNC_DELAY,
- },
- });
- }
- return noteItem;
- }
-}
-
-export default NoteImport;
diff --git a/src/note/noteParse.ts b/src/note/noteParse.ts
deleted file mode 100644
index bd69fe8..0000000
--- a/src/note/noteParse.ts
+++ /dev/null
@@ -1,832 +0,0 @@
-/*
- * This file realizes note parse (md, html, rich-text).
- */
-
-import TreeModel = require("tree-model");
-const asciidoctor = require("asciidoctor")();
-import YAML = require("yamljs");
-import AddonBase from "../module";
-import BetterNotes from "../addon";
-import { NodeMode } from "../sync/syncUtils";
-
-class NoteParse extends AddonBase {
- tools: any;
- constructor(parent: BetterNotes) {
- super(parent);
- }
-
- public parseNoteTree(
- note: Zotero.Item,
- parseLink: boolean = true
- ): TreeModel.Node {
- const noteLines = this._Addon.NoteUtils.getLinesInNote(note);
- let tree = new TreeModel();
- let root = tree.parse({
- id: -1,
- rank: 0,
- lineIndex: -1,
- endIndex: -1,
- });
- let id = 0;
- let lastNode = root;
- let headerStartReg = new RegExp("");
- for (let i in noteLines) {
- let currentRank = 7;
- let lineElement = noteLines[i];
- const isHeading = lineElement.search(headerStartReg) !== -1;
- const isLink =
- parseLink && lineElement.search(/zotero:\/\/note\//g) !== -1;
- if (isHeading || isLink) {
- let name = "";
- let link = "";
- if (isHeading) {
- const startIndex = lineElement.search(headerStartReg);
- currentRank = parseInt(
- lineElement.slice(startIndex + 2, startIndex + 3)
- );
- } else {
- link = lineElement.slice(lineElement.search(/href="/g) + 6);
- link = link.slice(0, link.search(/"/g));
- }
- name = this._Addon.NoteParse.parseLineText(lineElement);
-
- // Find parent node
- let parentNode = lastNode;
- while (parentNode.model.rank >= currentRank) {
- parentNode = parentNode.parent;
- }
-
- const currentNode = tree.parse({
- id: id++,
- rank: currentRank,
- name: name,
- lineIndex: parseInt(i),
- endIndex: noteLines.length - 1,
- link: link,
- });
- parentNode.addChild(currentNode);
- const currentIndex = parentNode.children.indexOf(currentNode);
- if (currentIndex > 0) {
- const previousNode = parentNode.children[currentIndex - 1];
- // Traverse the previous node tree and set the end index
- previousNode.walk((node) => {
- if (node.model.endIndex > parseInt(i) - 1) {
- node.model.endIndex = parseInt(i) - 1;
- }
- return true;
- });
- previousNode.model.endIndex = parseInt(i) - 1;
- }
- lastNode = currentNode;
- }
- }
- return root;
- }
- public parseHTMLLines(html: string): string[] {
- let containerIndex = html.search(/data-schema-version="[0-9]*">/g);
- if (containerIndex != -1) {
- html = html.substring(
- containerIndex + 'data-schema-version="8">'.length,
- html.length - " ".length
- );
- }
- let noteLines = html.split("\n").filter((e) => e);
-
- // A cache for temporarily stored lines
- let previousLineCache = [];
- let nextLineCache = [];
-
- const forceInline = ["table", "blockquote", "pre"];
- const selfInline = ["ol", "ul", "li"];
- let forceInlineStack = [];
- let forceInlineFlag = false;
- let selfInlineFlag = false;
-
- const parsedLines = [];
- for (let line of noteLines) {
- // restore self inline flag
- selfInlineFlag = false;
-
- // For force inline tags, set flag to append lines to current line
- for (const tag of forceInline) {
- const startReg = `<${tag}`;
- const isStart = line.includes(startReg);
- const endReg = `${tag}>`;
- const isEnd = line.includes(endReg);
- if (isStart && !isEnd) {
- forceInlineStack.push(tag);
- forceInlineFlag = true;
- break;
- }
- if (isEnd) {
- forceInlineStack.pop();
- // Exit force inline mode if the stack is empty
- if (forceInlineStack.length === 0) {
- forceInlineFlag = false;
- }
- break;
- }
- }
-
- if (forceInlineFlag) {
- nextLineCache.push(line);
- } else {
- // For self inline tags, cache start as previous line and end as next line
- for (const tag of selfInline) {
- const isStart = line.includes(`<${tag}`);
- const isEnd = line.includes(`${tag}>`);
- if (isStart && !isEnd) {
- selfInlineFlag = true;
- nextLineCache.push(line);
- break;
- }
- if (!isStart && isEnd) {
- selfInlineFlag = true;
- previousLineCache.push(line);
- break;
- }
- }
-
- if (!selfInlineFlag) {
- // Append cache to previous line
- if (previousLineCache.length) {
- parsedLines[parsedLines.length - 1] += `\n${previousLineCache.join(
- "\n"
- )}`;
- previousLineCache = [];
- }
- let nextLine = "";
- // Append cache to next line
- if (nextLineCache.length) {
- nextLine = nextLineCache.join("\n");
- nextLineCache = [];
- }
- if (nextLine) {
- nextLine += "\n";
- }
- nextLine += `${line}`;
- parsedLines.push(nextLine);
- }
- }
- }
- return parsedLines;
- }
-
- parseHTMLElements(doc: HTMLElement): Element[] {
- let currentLineIndex = 0;
- let currentElement: Element;
- let elements: Element[] = [];
-
- const diveTagNames = ["OL", "UL", "LI"];
- for (const e of doc.children) {
- if (diveTagNames.includes(e.tagName)) {
- const innerLines = this.parseListElements(e as HTMLElement);
- currentLineIndex += innerLines.length;
- currentElement = innerLines[innerLines.length - 1];
- elements = elements.concat(innerLines);
- } else {
- currentLineIndex += 1;
- currentElement = e;
- elements.push(e);
- }
- }
- return elements;
- }
-
- parseHTMLLineElement(doc: HTMLElement, lineIndex: number): HTMLElement {
- let currentLineIndex = 0;
- let currentElement: HTMLElement;
-
- const diveTagNames = ["OL", "UL", "LI"];
- for (const e of doc.children) {
- if (currentLineIndex > lineIndex) {
- break;
- }
- if (diveTagNames.includes(e.tagName)) {
- const innerLines = this.parseListElements(e as HTMLElement);
- if (currentLineIndex + innerLines.length > lineIndex) {
- // The target line is inside the line list
- for (const _e of innerLines) {
- if (currentLineIndex <= lineIndex) {
- currentLineIndex += 1;
- currentElement = _e;
- }
- }
- } else {
- currentLineIndex += innerLines.length;
- currentElement = innerLines[innerLines.length - 1];
- }
- } else {
- currentLineIndex += 1;
- currentElement = e as HTMLElement;
- }
- }
- this._Addon.toolkit.Tool.log(currentLineIndex);
- return currentElement;
- }
-
- async parseAnnotation(annotationItem: Zotero.Item) {
- try {
- if (!annotationItem || !annotationItem.isAnnotation()) {
- return null;
- }
- let json: AnnotationJson = await Zotero.Annotations.toJSON(
- annotationItem
- );
- json.id = annotationItem.key;
- json.attachmentItemID = annotationItem.parentItem.id;
- delete json.key;
- for (let key in json) {
- json[key] = json[key] || "";
- }
- json.tags = json.tags || [];
- return json;
- } catch (e) {
- Zotero.logError(e);
- return null;
- }
- }
-
- // Zotero.EditorInstanceUtilities.serializeAnnotations
- serializeAnnotations(
- annotations: AnnotationJson[],
- 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.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
- 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 = `${formatted} `;
- }
-
- // Image
- if (annotation.imageAttachmentKey) {
- // // let imageAttachmentKey = await this._importImage(annotation.image);
- // delete annotation.image;
-
- // 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 = ` `;
- }
-
- // Text
- if (annotation.text) {
- let text = Zotero.EditorInstanceUtilities._transformTextToHTML.call(
- Zotero.EditorInstanceUtilities,
- annotation.text.trim()
- );
- highlightHTML = `${text} `;
- quotedHighlightHTML = `${Zotero.getString(
- "punctuation.openingQMark"
- )}${text}${Zotero.getString("punctuation.closingQMark")} `;
- }
-
- // Note
- if (annotation.comment) {
- commentHTML = Zotero.EditorInstanceUtilities._transformTextToHTML.call(
- Zotero.EditorInstanceUtilities,
- annotation.comment.trim()
- );
- }
-
- let template;
- if (annotation.type === "highlight") {
- template = Zotero.Prefs.get("annotations.noteTemplates.highlight");
- } else if (annotation.type === "note") {
- template = Zotero.Prefs.get("annotations.noteTemplates.note");
- } else if (annotation.type === "image") {
- template = "{{image}} {{citation}} {{comment}}
";
- }
-
- this._Addon.toolkit.Tool.log("Using note template:");
- this._Addon.toolkit.Tool.log(template);
-
- template = template.replace(
- /([^<>]*?)({{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) =>
- attrs.quotes === "false" ? highlightHTML : quotedHighlightHTML,
- comment: commentHTML,
- citation: citationHTML,
- image: imageHTML,
- tags: (attrs) =>
- (
- (annotation.tags && annotation.tags.map((tag) => 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 };
- }
-
- async parseAnnotationHTML(
- note: Zotero.Item, // If you are sure there are no image annotations, note is not required.
- annotations: Zotero.Item[],
- ignoreComment: boolean = false,
- skipCitation: boolean = false
- ) {
- let annotationJSONList: AnnotationJson[] = [];
- for (const annot of annotations) {
- const annotJson = await this._Addon.NoteParse.parseAnnotation(annot);
- if (ignoreComment && annotJson.comment) {
- annotJson.comment = "";
- }
- annotationJSONList.push(annotJson);
- }
- await this._Addon.NoteUtils.importAnnotationImagesToNote(
- note,
- annotationJSONList
- );
- const html = this.serializeAnnotations(
- annotationJSONList,
- false,
- skipCitation
- ).html;
- return html;
- }
-
- async parseCitationHTML(citationIds: number[]) {
- let html = "";
- let items = await Zotero.Items.getAsync(citationIds);
- for (let item of items) {
- if (
- item.isNote() &&
- !(await Zotero.Notes.ensureEmbeddedImagesAreAvailable(item)) &&
- !Zotero.Notes.promptToIgnoreMissingImage()
- ) {
- return null;
- }
- }
-
- for (let item of items) {
- if (item.isRegularItem()) {
- let itemData = Zotero.Utilities.Item.itemToCSLJSON(item);
- let citation = {
- citationItems: [
- {
- uris: [Zotero.URI.getItemURI(item)],
- itemData,
- },
- ],
- properties: {},
- };
- let formatted = Zotero.EditorInstanceUtilities.formatCitation(citation);
- html += `${formatted}
`;
- }
- }
- return html;
- }
-
- async parseNoteStyleHTML(item: Zotero.Item, lineCount: number = 5) {
- if (!item.isNote()) {
- throw new Error("Item is not a note");
- }
- let note = `${this.parseHTMLLines(
- item.getNote()
- )
- .slice(0, lineCount)
- .join("\n")}
`;
-
- const parser = this._Addon.toolkit.Compat.getDOMParser();
- let doc = parser.parseFromString(note, "text/html");
-
- // Make sure this is the new note
- let metadataContainer = doc.querySelector(
- "body > div[data-schema-version]"
- );
- if (metadataContainer) {
- // Load base64 image data into src
- let nodes = doc.querySelectorAll(
- "img[data-attachment-key]"
- ) as NodeListOf;
- for (let node of nodes) {
- node.remove();
- }
-
- nodes = doc.querySelectorAll("span[style]") as NodeListOf;
- for (let node of nodes) {
- // Browser converts #RRGGBBAA hex color to rgba function, and we convert it to rgb function,
- // because word processors don't understand colors with alpha channel
- if (
- node.style.backgroundColor &&
- node.style.backgroundColor.startsWith("rgba")
- ) {
- node.style.backgroundColor =
- node.style.backgroundColor
- .replace("rgba", "rgb")
- .split(",")
- .slice(0, 3)
- .join(",") + ")";
- }
- }
- }
- return doc.body.innerHTML;
- }
-
- parseLinkInText(text: string): string {
- // Must end with "
- const linkIndex = text.search(/zotero:\/\/note\//g);
- if (linkIndex === -1) {
- return "";
- }
- let link = text.substring(linkIndex);
- link = link.substring(0, link.search('"'));
- return link;
- }
-
- parseLinkIndexInText(text: string): [number, number] {
- // Must end with "
- const linkIndex = text.search(/zotero:\/\/note\//g);
- if (linkIndex === -1) {
- return [-1, -1];
- }
- let link = text.substring(linkIndex);
- return [linkIndex, linkIndex + link.search('"')];
- }
-
- parseParamsFromLink(uri: string) {
- uri = uri.split("//").pop();
- const extraParams = {};
- uri
- .split("?")
- .pop()
- .split("&")
- .forEach((p) => {
- extraParams[p.split("=")[0]] = p.split("=")[1];
- });
- uri = uri.split("?")[0];
- let params: any = {
- libraryID: "",
- noteKey: 0,
- };
- Object.assign(params, extraParams);
- const router = new Zotero.Router(params);
- router.add("note/:libraryID/:noteKey", function () {
- if (params.libraryID === "u") {
- params.libraryID = Zotero.Libraries.userLibraryID;
- } else {
- params.libraryID = Zotero.Groups.getLibraryIDFromGroupID(
- params.libraryID
- );
- }
- });
- router.run(uri);
- return params;
- }
-
- parseListElements(
- e: HTMLElement,
- eleList: HTMLElement[] = undefined,
- tags: string[] = ["OL", "UL", "LI"]
- ) {
- if (!eleList) {
- eleList = [];
- }
- for (let _e of e.children) {
- if (tags.includes(_e.tagName)) {
- this.parseListElements(_e as HTMLElement, eleList);
- } else {
- eleList.push(e);
- }
- }
- return eleList;
- }
-
- parseNoteHTML(note: Zotero.Item): HTMLElement {
- if (!note) {
- return undefined;
- }
- let noteText = note.getNote();
- if (noteText.search(/data-schema-version/g) === -1) {
- noteText = `${noteText}\n
`;
- }
- const parser = this._Addon.toolkit.Compat.getDOMParser();
- let doc = parser.parseFromString(noteText, "text/html");
-
- let metadataContainer: HTMLElement = doc.querySelector(
- "body > div[data-schema-version]"
- );
- return metadataContainer;
- }
-
- parseLineText(line: string): string {
- const parser = this._Addon.toolkit.Compat.getDOMParser();
- try {
- if (line.search(/data-schema-version/g) === -1) {
- line = `${line}
`;
- }
- return (
- parser
- .parseFromString(line, "text/html")
- .querySelector("body > div[data-schema-version]") as HTMLElement
- ).innerText.trim();
- } catch (e) {
- return "";
- }
- }
-
- async parseMDToHTML(str: string): Promise {
- return await this._Addon.SyncUtils.md2note(str.replace(/\u00A0/gu, " "));
- }
-
- async parseHTMLToMD(str: string): Promise {
- return await this._Addon.SyncUtils.note2md(str);
- }
-
- parseAsciiDocToHTML(str: string): string {
- return asciidoctor.convert(str);
- }
-
- parseNoteToFreemind(
- noteItem: Zotero.Item,
- options: { withContent?: boolean } = { withContent: true }
- ) {
- const root = this.parseNoteTree(noteItem, false);
- const textNodeForEach = (e: Node, callbackfn: Function) => {
- if (e.nodeType === document.TEXT_NODE) {
- callbackfn(e);
- return;
- }
- e.childNodes.forEach((_e) => textNodeForEach(_e, callbackfn));
- };
- const html2Escape = (sHtml: string) => {
- return sHtml.replace(/[<>&"]/g, function (c) {
- return { "<": "<", ">": ">", "&": "&", '"': """ }[c];
- });
- };
- let lines = [];
- if (options.withContent) {
- const instance = this._Addon.WorkspaceWindow.getEditorInstance(noteItem);
- const editorCopy = this._Addon.EditorViews.getEditorElement(
- instance._iframeWindow.document
- ).cloneNode(true) as HTMLElement;
- textNodeForEach(editorCopy, (e: Text) => {
- e.data = html2Escape(e.data);
- });
- lines = this.parseHTMLElements(editorCopy as HTMLElement);
- }
- const convertClosingTags = (htmlStr: string) => {
- const regConfs = [
- {
- reg: / ]*?>/g,
- cbk: (str) => " ",
- },
- {
- reg: / ]*?>/g,
- cbk: (str: string) => {
- return ` `;
- },
- },
- ];
- for (const regConf of regConfs) {
- htmlStr = htmlStr.replace(regConf.reg, regConf.cbk);
- }
- return htmlStr;
- };
- const convertNode = (node: TreeModel.Node) => {
- mmXML += ` `;
- if (
- options.withContent &&
- node.model.lineIndex >= 0 &&
- node.model.endIndex >= 0
- ) {
- mmXML += `${convertClosingTags(
- lines
- .slice(
- node.model.lineIndex,
- node.hasChildren()
- ? node.children[0].model.lineIndex
- : node.model.endIndex + 1
- )
- .map((e) => e.outerHTML)
- .join("\n")
- )} `;
- }
- if (node.hasChildren()) {
- node.children.forEach((child: TreeModel.Node) => {
- convertNode(child);
- });
- }
- mmXML += " ";
- };
- let mmXML = '';
- convertNode(root);
- mmXML += " ";
- this._Addon.toolkit.Tool.log(mmXML);
- return mmXML;
- }
-
- async parseNoteToMD(
- noteItem: Zotero.Item,
- options: {
- withMeta?: boolean;
- skipSavingImages?: boolean;
- backend?: "turndown" | "unified";
- } = {}
- ) {
- const noteStatus = this._Addon.SyncUtils.getNoteStatus(noteItem);
- const rehype = this._Addon.SyncUtils.note2rehype(noteStatus.content);
- this._Addon.toolkit.Tool.log(rehype);
- this._Addon.SyncUtils.processN2MRehypeHighlightNodes(
- this._Addon.SyncUtils.getN2MRehypeHighlightNodes(rehype),
- NodeMode.direct
- );
- this._Addon.SyncUtils.processN2MRehypeCitationNodes(
- this._Addon.SyncUtils.getN2MRehypeCitationNodes(rehype),
- NodeMode.direct
- );
- this._Addon.SyncUtils.processN2MRehypeNoteLinkNodes(
- this._Addon.SyncUtils.getN2MRehypeNoteLinkNodes(rehype),
- this._Addon.NoteExport._exportFileInfo,
- NodeMode.direct
- );
- await this._Addon.SyncUtils.processN2MRehypeImageNodes(
- this._Addon.SyncUtils.getN2MRehypeImageNodes(rehype),
- noteItem.libraryID,
- this._Addon.NoteExport._exportPath,
- options.skipSavingImages,
- false,
- NodeMode.direct
- );
- this._Addon.toolkit.Tool.log("rehype", rehype);
- const remark = await this._Addon.SyncUtils.rehype2remark(rehype);
- this._Addon.toolkit.Tool.log("remark", remark);
- let md = this._Addon.SyncUtils.remark2md(remark);
-
- if (options.withMeta) {
- let header = {};
- try {
- header = JSON.parse(
- await this._Addon.TemplateController.renderTemplateAsync(
- "[ExportMDFileHeader]",
- "noteItem",
- [noteItem]
- )
- );
- } catch (e) {}
- Object.assign(header, {
- version: noteItem._version,
- libraryID: noteItem.libraryID,
- itemKey: noteItem.key,
- });
- let yamlFrontMatter = `---\n${YAML.stringify(header, 10)}\n---`;
- md = `${yamlFrontMatter}\n${md}`;
- }
- this._Addon.toolkit.Tool.log(md);
- return md;
- }
-
- async parseMDToNote(
- mdStatus: MDStatus,
- noteItem: Zotero.Item,
- isImport: boolean = false
- ) {
- this._Addon.toolkit.Tool.log("md", mdStatus);
- const remark = this._Addon.SyncUtils.md2remark(mdStatus.content);
- this._Addon.toolkit.Tool.log("remark", remark);
- const _rehype = await this._Addon.SyncUtils.remark2rehype(remark);
- this._Addon.toolkit.Tool.log("_rehype", _rehype);
- const _note = this._Addon.SyncUtils.rehype2note(_rehype);
- this._Addon.toolkit.Tool.log("_note", _note);
- const rehype = this._Addon.SyncUtils.note2rehype(_note);
- this._Addon.toolkit.Tool.log("rehype", rehype);
-
- // Check if image already belongs to note
- this._Addon.SyncUtils.processM2NRehypeMetaImageNodes(
- this._Addon.SyncUtils.getM2NRehypeImageNodes(rehype)
- );
-
- this._Addon.SyncUtils.processM2NRehypeHighlightNodes(
- this._Addon.SyncUtils.getM2NRehypeHighlightNodes(rehype)
- );
- await this._Addon.SyncUtils.processM2NRehypeCitationNodes(
- this._Addon.SyncUtils.getM2NRehypeCitationNodes(rehype),
- isImport
- );
- this._Addon.SyncUtils.processM2NRehypeNoteLinkNodes(
- this._Addon.SyncUtils.getM2NRehypeNoteLinkNodes(rehype)
- );
- await this._Addon.SyncUtils.processM2NRehypeImageNodes(
- this._Addon.SyncUtils.getM2NRehypeImageNodes(rehype),
- noteItem,
- mdStatus.filedir,
- isImport
- );
- this._Addon.toolkit.Tool.log(rehype);
- const noteContent = this._Addon.SyncUtils.rehype2note(rehype);
- return noteContent;
- }
-
- async parseNoteForDiff(noteItem: Zotero.Item) {
- const noteStatus = this._Addon.SyncUtils.getNoteStatus(noteItem);
- const rehype = this._Addon.SyncUtils.note2rehype(noteStatus.content);
- await this._Addon.SyncUtils.processM2NRehypeCitationNodes(
- this._Addon.SyncUtils.getM2NRehypeCitationNodes(rehype),
- true
- );
- // Prse content like ciations
- return this._Addon.SyncUtils.rehype2note(rehype);
- }
-}
-
-export default NoteParse;
diff --git a/src/note/noteUtils.ts b/src/note/noteUtils.ts
deleted file mode 100644
index e00c989..0000000
--- a/src/note/noteUtils.ts
+++ /dev/null
@@ -1,832 +0,0 @@
-/*
- * This file realizes note tools.
- */
-
-import TreeModel = require("tree-model");
-import BetterNotes from "../addon";
-import AddonBase from "../module";
-
-class NoteUtils extends AddonBase {
- public currentLine: any;
- constructor(parent: BetterNotes) {
- super(parent);
- this.currentLine = {};
- }
-
- public getLinesInNote(note: Zotero.Item): string[] {
- if (!note) {
- return [];
- }
- let noteText: string = note.getNote();
- return this._Addon.NoteParse.parseHTMLLines(noteText);
- }
-
- public async setLinesToNote(note: Zotero.Item, noteLines: string[]) {
- if (!note) {
- return [];
- }
- let noteText: string = note.getNote();
- let containerIndex = noteText.search(/data-schema-version="[0-9]*/g);
- if (containerIndex === -1) {
- note.setNote(
- `${noteLines.join("\n")}
`
- );
- } else {
- let noteHead = noteText.substring(0, containerIndex);
- note.setNote(
- `${noteHead}data-schema-version="8">${noteLines.join("\n")}`
- );
- }
-
- await note.saveTx();
- }
-
- public async addLineToNote(
- note: Zotero.Item,
- text: string,
- lineIndex: number,
- forceMetadata: boolean = false,
- position: "before" | "after" = "after"
- ) {
- if (!note) {
- return;
- }
- let noteLines = this.getLinesInNote(note);
- let autoLineIndex = false;
- if (lineIndex < 0) {
- autoLineIndex = true;
- lineIndex = this.currentLine[note.id];
- lineIndex = lineIndex && lineIndex >= 0 ? lineIndex : noteLines.length;
- } else if (lineIndex >= noteLines.length) {
- lineIndex = noteLines.length;
- }
- Zotero.debug(
- `insert to ${lineIndex}, it used to be ${noteLines[lineIndex]}`
- );
- this._Addon.toolkit.Tool.log(text);
-
- const editorInstance = this._Addon.WorkspaceWindow.getEditorInstance(note);
- if (editorInstance && !forceMetadata) {
- // The note is opened. Add line via note editor
- this._Addon.toolkit.Tool.log("Add note line via note editor");
- const _document = editorInstance._iframeWindow.document;
- const currentElement = this._Addon.NoteParse.parseHTMLLineElement(
- this._Addon.EditorViews.getEditorElement(_document) as HTMLElement,
- lineIndex
- );
- const frag = _document.createDocumentFragment();
- const temp = this._Addon.toolkit.UI.createElement(
- _document,
- "div",
- "html",
- false
- ) as HTMLDivElement;
- temp.innerHTML = text;
- while (temp.firstChild) {
- frag.appendChild(temp.firstChild);
- }
- const defer = Zotero.Promise.defer();
- const notifyName = `addLineToNote-${note.id}`;
- this._Addon.ZoteroNotifies.registerNotifyListener(
- notifyName,
- (
- event: string,
- type: string,
- ids: Array,
- extraData: object
- ) => {
- if (event === "modify" && type === "item" && ids.includes(note.id)) {
- this._Addon.ZoteroNotifies.unregisterNotifyListener(notifyName);
- defer.resolve();
- }
- }
- );
- position === "after"
- ? currentElement.after(frag)
- : currentElement.before(frag);
-
- await defer.promise;
-
- this._Addon.EditorViews.scrollToPosition(
- editorInstance,
- currentElement.offsetTop
- );
- } else {
- // The note editor does not exits yet. Fall back to modify the metadata
- this._Addon.toolkit.Tool.log("Add note line via note metadata");
-
- // insert after/before current line
- if (position === "after") {
- lineIndex += 1;
- }
- noteLines.splice(lineIndex, 0, text);
- await this.setLinesToNote(note, noteLines);
- if (this._Addon.WorkspaceWindow.getWorkspaceNote().id === note.id) {
- await this.scrollWithRefresh(lineIndex);
- }
- }
-
- if (autoLineIndex) {
- // Compute annotation lines length
- const temp = this._Addon.toolkit.UI.createElement(
- document,
- "div"
- ) as HTMLDivElement;
- temp.innerHTML = text;
- const elementList = this._Addon.NoteParse.parseHTMLElements(temp);
- // Move cursor foward
- this._Addon.NoteUtils.currentLine[note.id] += elementList.length;
- }
- }
-
- private _dataURLtoBlob(dataurl: string) {
- let parts = dataurl.split(",");
- let matches = parts[0]?.match(/:(.*?);/);
- if (!matches || !matches[1]) {
- return;
- }
- let mime = matches[1];
- if (parts[0].indexOf("base64") !== -1) {
- let bstr = atob(parts[1]);
- let n = bstr.length;
- let u8arr = new Uint8Array(n);
- while (n--) {
- u8arr[n] = bstr.charCodeAt(n);
- }
-
- return new (this._Addon.toolkit.Compat.getGlobal("Blob") as typeof Blob)(
- [u8arr],
- {
- type: mime,
- }
- );
- }
- return null;
- }
-
- public async importImageToNote(
- note: Zotero.Item,
- src: string,
- type: "b64" | "url" | "file" = "b64"
- ): Promise {
- if (!note || !note.isNote()) {
- return "";
- }
- let blob: Blob;
- if (src.startsWith("data:")) {
- blob = this._dataURLtoBlob(src);
- } else if (type === "url") {
- let res;
- try {
- res = await Zotero.HTTP.request("GET", src, { responseType: "blob" });
- } catch (e) {
- return;
- }
- blob = res.response;
- } else if (type === "file") {
- src = Zotero.File.normalizeToUnix(src);
- const noteAttachmentKeys = Zotero.Items.get(note.getAttachments()).map(
- (_i) => _i.key
- );
- const filename = src.split("/").pop().split(".").shift();
- // The exported image is KEY.png by default.
- // If it is already an attachment, just keep it.
- if (noteAttachmentKeys.includes(filename)) {
- return filename;
- }
- const imageData = await Zotero.File.getBinaryContentsAsync(src);
- const array = new Uint8Array(imageData.length);
- for (let i = 0; i < imageData.length; i++) {
- array[i] = imageData.charCodeAt(i);
- }
- blob = new Blob([array], { type: "image/png" });
- } else {
- return;
- }
-
- let attachment = await Zotero.Attachments.importEmbeddedImage({
- blob,
- parentItemID: note.id,
- saveOptions: {},
- });
-
- return attachment.key;
- }
-
- public async importAnnotationImagesToNote(
- note: Zotero.Item,
- annotations: AnnotationJson[]
- ) {
- for (let annotation of annotations) {
- if (annotation.image) {
- annotation.imageAttachmentKey =
- (await this.importImageToNote(note, annotation.image)) || "";
- }
- delete annotation.image;
- }
- }
-
- public async addAnnotationsToNote(
- note: Zotero.Item,
- annotations: Zotero.Item[],
- lineIndex: number,
- showMsg: boolean = false
- ) {
- if (!note) {
- return;
- }
- const html = await this._Addon.NoteParse.parseAnnotationHTML(
- note,
- annotations
- );
- await this.addLineToNote(note, html, lineIndex, showMsg);
- const noteTitle = note.getNoteTitle();
- this._Addon.ZoteroViews.showProgressWindow(
- "Better Notes",
- `Insert lines into ${
- lineIndex >= 0 ? `line ${lineIndex} in` : "end of"
- } note ${
- noteTitle.length > 15 ? noteTitle.slice(0, 12) + "..." : noteTitle
- }`
- );
- return html;
- }
-
- public async addLinkToNote(
- targetNote: Zotero.Item,
- linkedNote: Zotero.Item,
- lineIndex: number,
- sectionName: string
- ) {
- if (!targetNote) {
- return;
- }
- if (!linkedNote.isNote()) {
- this._Addon.ZoteroViews.showProgressWindow(
- "Better Notes",
- "Not a note item"
- );
- return;
- }
- const link = this.getNoteLink(linkedNote);
- const linkText = linkedNote.getNoteTitle().trim();
-
- const linkTemplate =
- await this._Addon.TemplateController.renderTemplateAsync(
- "[QuickInsert]",
- "link, subNoteItem, noteItem, sectionName, lineIndex",
- [link, linkedNote, targetNote, sectionName, lineIndex]
- );
-
- this.addLineToNote(targetNote, linkTemplate, lineIndex);
-
- const backLinkTemplate =
- await this._Addon.TemplateController.renderTemplateAsync(
- "[QuickBackLink]",
- "subNoteItem, noteItem, sectionName, lineIndex",
- [linkedNote, targetNote, sectionName, lineIndex],
- false
- );
-
- if (backLinkTemplate) {
- this.addLineToNote(linkedNote, backLinkTemplate, -1);
- }
-
- this._Addon.ZoteroViews.showProgressWindow(
- "Better Notes",
- `Link is added to ${lineIndex >= 0 ? ` line ${lineIndex}` : ""}`
- );
- }
-
- public getNoteLink(
- note: Zotero.Item,
- options: {
- ignore?: boolean;
- withLine?: boolean;
- } = { ignore: false, withLine: false }
- ) {
- let libraryID = note.libraryID;
- let library = Zotero.Libraries.get(libraryID) as Zotero.Library;
- let groupID: string;
- if (library.libraryType === "user") {
- groupID = "u";
- } else if (library.libraryType === "group") {
- groupID = `${library.id}`;
- } else {
- return "";
- }
- let noteKey = note.key;
- let link = `zotero://note/${groupID}/${noteKey}/`;
- const addParam = (link: string, param: string): string => {
- const lastChar = link[link.length - 1];
- if (lastChar === "/") {
- link += "?";
- } else if (lastChar !== "?" && lastChar !== "&") {
- link += "&";
- }
- return `${link}${param}`;
- };
- if (options.ignore || options.withLine) {
- if (options.ignore) {
- link = addParam(link, "ignore=1");
- }
- if (options.withLine) {
- if (!this.currentLine[note.id]) {
- this.currentLine[note.id] = 0;
- }
- link = addParam(link, `line=${this.currentLine[note.id]}`);
- }
- }
- return link;
- }
-
- public getAnnotationLink(annotation: Zotero.Item) {
- let position = JSON.parse(annotation.annotationPosition);
- let openURI: string;
-
- const attachment = annotation.parentItem;
- let libraryID = attachment.libraryID;
- let library = Zotero.Libraries.get(libraryID) as Zotero.Library;
- if (library.libraryType === "user") {
- openURI = `zotero://open-pdf/library/items/${attachment.key}`;
- } else if (library.libraryType === "group") {
- openURI = `zotero://open-pdf/groups/${library.id}/items/${attachment.key}`;
- } else {
- openURI = "";
- }
-
- openURI +=
- "?page=" +
- (position.pageIndex + 1) +
- (annotation.key ? "&annotation=" + annotation.key : "");
-
- return openURI;
- }
-
- async modifyLineInNote(
- note: Zotero.Item,
- text: string | Function,
- lineIndex: number,
- forceMetadata: boolean = false
- ) {
- if (!note) {
- return;
- }
- let noteLines = this.getLinesInNote(note);
- if (lineIndex < 0 || lineIndex >= noteLines.length) {
- return;
- }
- if (typeof text === "string") {
- noteLines[lineIndex] = text;
- } else if (typeof text === "function") {
- noteLines[lineIndex] = text(noteLines[lineIndex]);
- }
- const editorInstance = this._Addon.WorkspaceWindow.getEditorInstance(note);
- if (editorInstance && !forceMetadata) {
- // The note is opened. Add line via note editor
- this._Addon.toolkit.Tool.log("Modify note line via note editor");
- const _document = editorInstance._iframeWindow.document;
- const currentElement: HTMLElement =
- this._Addon.NoteParse.parseHTMLLineElement(
- this._Addon.EditorViews.getEditorElement(_document) as HTMLElement,
- lineIndex
- );
- const frag = _document.createDocumentFragment();
- const temp = this._Addon.toolkit.UI.createElement(
- _document,
- "div",
- "html",
- false
- ) as HTMLDivElement;
- temp.innerHTML = noteLines[lineIndex];
- while (temp.firstChild) {
- frag.appendChild(temp.firstChild);
- }
- const defer = Zotero.Promise.defer();
- const notifyName = `modifyLineInNote-${note.id}`;
- this._Addon.ZoteroNotifies.registerNotifyListener(
- notifyName,
- (
- event: string,
- type: string,
- ids: Array,
- extraData: object
- ) => {
- if (event === "modify" && type === "item" && ids.includes(note.id)) {
- this._Addon.ZoteroNotifies.unregisterNotifyListener(notifyName);
- defer.resolve();
- }
- }
- );
- currentElement.replaceWith(frag);
- await defer.promise;
- this._Addon.EditorViews.scrollToPosition(
- editorInstance,
- currentElement.offsetTop
- );
- } else {
- await this.setLinesToNote(note, noteLines);
- await this.scrollWithRefresh(lineIndex);
- }
- }
-
- async moveHeaderLineInNote(
- note: Zotero.Item,
- currentNode: TreeModel.Node,
- targetNode: TreeModel.Node,
- as: "child" | "before" | "after"
- ) {
- if (!note || targetNode.getPath().indexOf(currentNode) >= 0) {
- return;
- }
-
- let targetIndex = 0;
- let targetRank = 1;
-
- let lines = this.getLinesInNote(note);
-
- if (as === "child") {
- targetIndex = targetNode.model.endIndex;
- targetRank = targetNode.model.rank === 6 ? 6 : targetNode.model.rank + 1;
- } else if (as === "before") {
- targetIndex = targetNode.model.lineIndex - 1;
- targetRank =
- targetNode.model.rank === 7
- ? targetNode.parent.model.rank === 6
- ? 6
- : targetNode.parent.model.rank + 1
- : targetNode.model.rank;
- } else if (as === "after") {
- targetIndex = targetNode.model.endIndex;
- targetRank =
- targetNode.model.rank === 7
- ? targetNode.parent.model.rank === 6
- ? 6
- : targetNode.parent.model.rank + 1
- : targetNode.model.rank;
- }
-
- let rankChange = targetRank - currentNode.model.rank;
-
- let movedLines = lines.splice(
- currentNode.model.lineIndex,
- currentNode.model.endIndex - currentNode.model.lineIndex + 1
- );
-
- let headerReg = /<\/?h[1-6]/g;
- for (const i in movedLines) {
- movedLines[i] = movedLines[i].replace(headerReg, (e) => {
- let rank = parseInt(e.slice(-1));
- rank += rankChange;
- if (rank > 6) {
- rank = 6;
- }
- if (rank < 1) {
- rank = 1;
- }
- return `${e.slice(0, -1)}${rank}`;
- });
- }
-
- // If the moved lines is before the insert index
- // the slice index -= lines length.
- if (currentNode.model.endIndex <= targetIndex) {
- targetIndex -= movedLines.length;
- }
- this._Addon.toolkit.Tool.log(`move to ${targetIndex}`);
-
- let newLines = lines
- .slice(0, targetIndex + 1)
- .concat(movedLines, lines.slice(targetIndex + 1));
- this._Addon.toolkit.Tool.log("new lines", newLines);
- this._Addon.toolkit.Tool.log("moved", movedLines);
- this._Addon.toolkit.Tool.log("insert after", lines[targetIndex]);
- this._Addon.toolkit.Tool.log("next line", lines[targetIndex + 1]);
- await this.setLinesToNote(note, newLines);
- }
-
- getNoteTree(note: Zotero.Item): TreeModel.Node {
- // See http://jnuno.com/tree-model-js
- if (!note) {
- return undefined;
- }
- return this._Addon.NoteParse.parseNoteTree(note);
- }
-
- getNoteTreeAsList(
- note: Zotero.Item,
- filterRoot: boolean = true,
- filterLink: boolean = true
- ): TreeModel.Node[] {
- if (!note) {
- return;
- }
- return this.getNoteTree(note).all(
- (node) =>
- (!filterRoot || node.model.lineIndex >= 0) &&
- (!filterLink || node.model.rank <= 6)
- );
- }
-
- getNoteTreeNodeById(
- note: Zotero.Item,
- id: number,
- root: TreeModel.Node = undefined
- ) {
- root = root || this.getNoteTree(note);
- return root.first(function (node) {
- return node.model.id === id;
- });
- }
-
- getNoteTreeNodesByRank(
- note: Zotero.Item,
- rank: number,
- root: TreeModel.Node = undefined
- ) {
- root = root || this.getNoteTree(note);
- return root.all(function (node) {
- return node.model.rank === rank;
- });
- }
-
- getLineParentNode(
- note: Zotero.Item,
- lineIndex: number = -1
- ): TreeModel.Node {
- if (lineIndex < 0) {
- lineIndex = this.currentLine[note.id];
- lineIndex =
- lineIndex && lineIndex >= 0
- ? lineIndex
- : this.getLinesInNote(note).length;
- }
- let nodes = this.getNoteTreeAsList(note);
- if (!nodes.length || nodes[0].model.lineIndex > lineIndex) {
- // There is no parent node
- return undefined;
- } else if (nodes[nodes.length - 1].model.lineIndex <= lineIndex) {
- return nodes[nodes.length - 1];
- } else {
- for (let i = 0; i < nodes.length - 1; i++) {
- if (
- nodes[i].model.lineIndex <= lineIndex &&
- nodes[i + 1].model.lineIndex > lineIndex
- ) {
- return nodes[i];
- }
- }
- }
- }
-
- async moveNode(fromID: number, toID: number, moveType: "before" | "child") {
- const workspaceNote = this._Addon.WorkspaceWindow.getWorkspaceNote();
- let tree = this.getNoteTree(workspaceNote);
- let fromNode = this.getNoteTreeNodeById(workspaceNote, fromID, tree);
- let toNode = this._Addon.NoteUtils.getNoteTreeNodeById(
- workspaceNote,
- toID,
- tree
- );
- this._Addon.toolkit.Tool.log(toNode.model, fromNode.model, moveType);
- this.moveHeaderLineInNote(
- this._Addon.WorkspaceWindow.getWorkspaceNote(),
- fromNode,
- toNode,
- moveType
- );
- }
-
- async scrollWithRefresh(lineIndex: number) {
- await Zotero.Promise.delay(500);
- let editorInstance =
- await this._Addon.WorkspaceWindow.getWorkspaceEditorInstance();
- if (!editorInstance) {
- return;
- }
- this._Addon.EditorViews.scrollToLine(editorInstance, lineIndex);
- }
-
- async convertNoteLines(
- currentNote: Zotero.Item,
- rootNoteIds: number[],
- convertNoteLinks: boolean = true
- ): Promise<{ lines: string[]; subNotes: Zotero.Item[] }> {
- this._Addon.toolkit.Tool.log(`convert note ${currentNote.id}`);
-
- let subNotes: Zotero.Item[] = [];
- const [..._rootNoteIds] = rootNoteIds;
- _rootNoteIds.push(currentNote.id);
-
- let newLines: string[] = [];
- const noteLines = this.getLinesInNote(currentNote);
- for (let i in noteLines) {
- newLines.push(noteLines[i]);
- // Convert Link
- if (convertNoteLinks) {
- let link = this._Addon.NoteParse.parseLinkInText(noteLines[i]);
- while (link) {
- const linkIndex = noteLines[i].indexOf(link);
- const params = this._Addon.NoteParse.parseParamsFromLink(link);
- if (
- params.ignore ||
- // Ignore links that are not in
- !noteLines[i].slice(linkIndex - 8, linkIndex).includes("href")
- ) {
- this._Addon.toolkit.Tool.log("ignore link");
- noteLines[i] = noteLines[i].substring(
- noteLines[i].search(/zotero:\/\/note\//g)
- );
- noteLines[i] = noteLines[i].substring(
- noteLines[i].search(/<\/a>/g) + " ".length
- );
- link = this._Addon.NoteParse.parseLinkInText(noteLines[i]);
- continue;
- }
- this._Addon.toolkit.Tool.log("convert link");
- let res = await this.getNoteFromLink(link);
- const subNote = res.item;
- if (subNote && _rootNoteIds.indexOf(subNote.id) === -1) {
- this._Addon.toolkit.Tool.log(
- `Knowledge4Zotero: Exporting sub-note ${link}`
- );
- const convertResult = await this.convertNoteLines(
- subNote,
- _rootNoteIds,
- convertNoteLinks
- );
- const subNoteLines = convertResult.lines;
-
- const templateText =
- await this._Addon.TemplateController.renderTemplateAsync(
- "[QuickImport]",
- "subNoteLines, subNoteItem, noteItem",
- [subNoteLines, subNote, currentNote]
- );
- newLines.push(templateText);
- subNotes.push(subNote);
- subNotes = subNotes.concat(convertResult.subNotes);
- }
- noteLines[i] = noteLines[i].substring(
- noteLines[i].search(/zotero:\/\/note\//g)
- );
- noteLines[i] = noteLines[i].substring(
- noteLines[i].search(/<\/a>/g) + "".length
- );
- link = this._Addon.NoteParse.parseLinkInText(noteLines[i]);
- }
- }
- }
- this._Addon.toolkit.Tool.log(subNotes);
- return { lines: newLines, subNotes: subNotes };
- }
-
- async getNoteFromLink(uri: string) {
- const params = this._Addon.NoteParse.parseParamsFromLink(uri);
- if (!params.libraryID) {
- return {
- item: undefined,
- args: {},
- infoText: "Library does not exist or access denied.",
- };
- }
- this._Addon.toolkit.Tool.log(params);
- let item: Zotero.Item = (await Zotero.Items.getByLibraryAndKeyAsync(
- params.libraryID,
- params.noteKey
- )) as Zotero.Item;
- if (!item || !item.isNote()) {
- return {
- item: undefined,
- args: params,
- infoText: "Note does not exist or is not a note.",
- };
- }
- return {
- item: item,
- args: params,
- infoText: "OK",
- };
- }
-
- public async onSelectionChange(
- editor: Zotero.EditorInstance,
- itemID: number
- ) {
- // Update current line index
- const _window = editor._iframeWindow;
- const selection = _window.document.getSelection();
- if (!selection || !selection.focusNode) {
- return;
- }
- const realElement = selection.focusNode.parentElement;
- let focusNode = selection.focusNode as XUL.Element;
- if (!focusNode || !realElement) {
- return;
- }
-
- function getChildIndex(node) {
- return Array.prototype.indexOf.call(node.parentNode.childNodes, node);
- }
-
- // Make sure this is a direct child node of editor
- try {
- while (
- focusNode.parentElement &&
- (!focusNode.parentElement.className ||
- focusNode.parentElement.className.indexOf("primary-editor") === -1)
- ) {
- focusNode = focusNode.parentNode as XUL.Element;
- }
- } catch (e) {
- return;
- }
-
- if (!focusNode.parentElement) {
- return;
- }
-
- let currentLineIndex = getChildIndex(focusNode);
-
- // Parse list
- const diveTagNames = ["OL", "UL", "LI"];
-
- // Find list elements before current line
- const listElements = Array.prototype.filter.call(
- Array.prototype.slice.call(
- focusNode.parentElement.childNodes,
- 0,
- currentLineIndex
- ),
- (e) => diveTagNames.includes(e.tagName)
- );
-
- for (const e of listElements) {
- currentLineIndex += this._Addon.NoteParse.parseListElements(e).length - 1;
- }
-
- // Find list index if current line is inside a list
- if (diveTagNames.includes(focusNode.tagName)) {
- const eleList = this._Addon.NoteParse.parseListElements(focusNode);
- for (const i in eleList) {
- if (realElement.parentElement === eleList[i]) {
- currentLineIndex += Number(i);
- break;
- }
- }
- }
- this._Addon.toolkit.Tool.log(
- `line ${currentLineIndex} of item ${itemID} selected.`
- );
- // Don't use the instance._item.id, as it might not be updated.
- this.currentLine[itemID] = currentLineIndex;
- if (realElement.tagName === "A") {
- let link = (realElement as HTMLLinkElement).href;
- let linkedNote = (await this.getNoteFromLink(link)).item;
- if (linkedNote) {
- let t = 0;
- let linkPopup = _window.document.querySelector(".link-popup");
- while (
- !(
- linkPopup &&
- (linkPopup.querySelector("a") as unknown as HTMLLinkElement)
- ?.href === link
- ) &&
- t < 100
- ) {
- t += 1;
- linkPopup = _window.document.querySelector(".link-popup");
- await Zotero.Promise.delay(30);
- }
- await this._Addon.EditorViews.updateEditorLinkPopup(editor, link);
- } else {
- await this._Addon.EditorViews.updateEditorLinkPopup(editor, undefined);
- }
- }
- if (_window.document.querySelector(".regular-image.selected")) {
- let t = 0;
- let imagePopup = _window.document.querySelector(".image-popup");
- while (!imagePopup && t < 100) {
- t += 1;
- imagePopup = _window.document.querySelector(".image-popup");
- await Zotero.Promise.delay(30);
- }
- if (imagePopup) {
- this._Addon.EditorViews.updateEditorImagePopup(editor);
- }
- }
- }
-
- public formatPath(path: string) {
- if (Zotero.isWin) {
- path = path.replace(/\\/g, "/");
- path = OS.Path.join(...path.split(/\//));
- }
- if (Zotero.isMac && path.charAt(0) !== "/") {
- path = "/" + path;
- }
- return path;
- }
-}
-
-export default NoteUtils;
diff --git a/src/reader/readerViews.ts b/src/reader/readerViews.ts
deleted file mode 100644
index b56caee..0000000
--- a/src/reader/readerViews.ts
+++ /dev/null
@@ -1,354 +0,0 @@
-/*
- * This file contains reader annotation pane code.
- */
-
-const CryptoJS = require("crypto-js");
-import BetterNotes from "../addon";
-import AddonBase from "../module";
-import { EditorMessage } from "../utils";
-
-class ReaderViews extends AddonBase {
- icons: object;
-
- constructor(parent: BetterNotes) {
- super(parent);
- this.icons = {
- createNote: ` `,
- ocrTex: ` `,
- };
- }
-
- async addReaderAnnotationButton(reader: _ZoteroReaderInstance) {
- if (!reader) {
- return false;
- }
- await reader._initPromise;
- let updateCount = 0;
- const _document = reader._iframeWindow.document;
- for (const moreButton of _document.getElementsByClassName("more")) {
- if (moreButton.getAttribute("knowledgeinit") === "true") {
- updateCount += 1;
- continue;
- }
- moreButton.setAttribute("knowledgeinit", "true");
- const createNoteButton = this._Addon.toolkit.UI.createElement(
- _document,
- "div"
- ) as HTMLDivElement;
- createNoteButton.setAttribute("style", "margin: 5px;");
- createNoteButton.title = "Quick Note";
- createNoteButton.innerHTML = this.icons["createNote"];
-
- let annotationWrapper = moreButton;
- while (!annotationWrapper.getAttribute("data-sidebar-annotation-id")) {
- annotationWrapper = annotationWrapper.parentElement;
- }
- const itemKey = annotationWrapper.getAttribute(
- "data-sidebar-annotation-id"
- );
- const libraryID = (Zotero.Items.get(reader.itemID) as Zotero.Item)
- .libraryID;
- const annotationItem = await Zotero.Items.getByLibraryAndKeyAsync(
- libraryID,
- itemKey
- );
-
- createNoteButton.addEventListener("click", async (e) => {
- await this.createNoteFromAnnotation(annotationItem, e);
- e.preventDefault();
- });
- createNoteButton.addEventListener("mouseover", (e: XUL.XULEvent) => {
- createNoteButton.setAttribute(
- "style",
- "background: #F0F0F0; margin: 5px;"
- );
- });
- createNoteButton.addEventListener("mouseout", (e: XUL.XULEvent) => {
- createNoteButton.setAttribute("style", "margin: 5px;");
- });
- moreButton.before(createNoteButton);
- if (annotationItem.annotationType === "image") {
- // Image OCR
- const ocrButton = this._Addon.toolkit.UI.createElement(
- _document,
- "div"
- ) as HTMLDivElement;
- ocrButton.setAttribute("style", "margin: 5px;");
- ocrButton.innerHTML = this.icons["ocrTex"];
- ocrButton.title = "OCR LaTex";
- ocrButton.addEventListener("click", async (e) => {
- await this.OCRImageAnnotation(
- (
- ocrButton.parentElement.parentElement
- .nextSibling as HTMLImageElement
- ).src,
- annotationItem
- );
-
- e.preventDefault();
- });
- ocrButton.addEventListener("mouseover", (e: XUL.XULEvent) => {
- ocrButton.setAttribute("style", "background: #F0F0F0; margin: 5px;");
- });
- ocrButton.addEventListener("mouseout", (e: XUL.XULEvent) => {
- ocrButton.setAttribute("style", "margin: 5px;");
- });
- moreButton.before(ocrButton);
- }
- updateCount += 1;
- }
- return reader.annotationItemIDs.length === updateCount;
- }
-
- public async buildReaderAnnotationButtons() {
- this._Addon.toolkit.Tool.log("buildReaderAnnotationButton")
- for (const reader of Zotero.Reader._readers) {
- this._Addon.toolkit.Tool.log("reader found");
- let t = 0;
- while (t < 100 && !(await this.addReaderAnnotationButton(reader))) {
- await Zotero.Promise.delay(50);
- t += 1;
- }
- }
- }
-
- private async createNoteFromAnnotation(
- annotationItem: Zotero.Item,
- event: MouseEvent
- ) {
- if (annotationItem.annotationComment) {
- const text = annotationItem.annotationComment;
- let link = this._Addon.NoteParse.parseLinkInText(text);
-
- if (link) {
- const note = (await this._Addon.NoteUtils.getNoteFromLink(link)).item;
- if (note && note.id) {
- await this._Addon.ZoteroEvents.onEditorEvent(
- new EditorMessage("onNoteLink", {
- params: {
- item: note,
- infoText: "OK",
- forceStandalone: event.shiftKey,
- },
- })
- );
- return;
- }
- }
- }
-
- const note: Zotero.Item = new Zotero.Item("note");
- note.libraryID = annotationItem.libraryID;
- note.parentID = annotationItem.parentItem.parentID;
- await note.saveTx();
-
- ZoteroPane.openNoteWindow(note.id);
- let editorInstance: Zotero.EditorInstance =
- this._Addon.WorkspaceWindow.getEditorInstance(note);
- let t = 0;
- // Wait for editor instance
- while (t < 10 && !editorInstance) {
- await Zotero.Promise.delay(500);
- t += 1;
- editorInstance = this._Addon.WorkspaceWindow.getEditorInstance(note);
- }
-
- const renderredTemplate =
- await this._Addon.TemplateController.renderTemplateAsync(
- "[QuickNoteV4]",
- "annotationItem, topItem, noteItem",
- [annotationItem, annotationItem.parentItem.parentItem, note]
- );
- await this._Addon.NoteUtils.addLineToNote(
- note,
- renderredTemplate,
- 0,
- false,
- "before"
- );
-
- const tags = annotationItem.getTags();
- for (const tag of tags) {
- note.addTag(tag.tag, tag.type);
- }
- await note.saveTx();
-
- annotationItem.annotationComment = `${
- annotationItem.annotationComment ? annotationItem.annotationComment : ""
- }\nnote link: "${this._Addon.NoteUtils.getNoteLink(note)}"`;
- await annotationItem.saveTx();
- }
-
- private async OCRImageAnnotation(src: string, annotationItem: Zotero.Item) {
- /*
- message.content = {
- params: { src: string, annotationItem: Zotero.Item }
- }
- */
- let result: string;
- let success: boolean;
- const engine = Zotero.Prefs.get("Knowledge4Zotero.OCREngine");
- if (engine === "mathpix") {
- const xhr = await Zotero.HTTP.request(
- "POST",
- "https://api.mathpix.com/v3/text",
- {
- headers: {
- "Content-Type": "application/json; charset=utf-8",
- app_id: Zotero.Prefs.get("Knowledge4Zotero.OCRMathpix.Appid"),
- app_key: Zotero.Prefs.get("Knowledge4Zotero.OCRMathpix.Appkey"),
- },
- body: JSON.stringify({
- src: src,
- math_inline_delimiters: ["$", "$"],
- math_display_delimiters: ["$$", "$$"],
- rm_spaces: true,
- }),
- responseType: "json",
- }
- );
- this._Addon.toolkit.Tool.log(xhr);
- if (xhr && xhr.status && xhr.status === 200 && xhr.response.text) {
- result = xhr.response.text;
- success = true;
- } else {
- result =
- xhr.status === 200 ? xhr.response.error : `${xhr.status} Error`;
- success = false;
- }
- } else if (engine === "xunfei") {
- /**
- * 1.Doc:https://www.xfyun.cn/doc/words/formula-discern/API.html
- * 2.Error code:https://www.xfyun.cn/document/error-code
- * @author iflytek
- */
-
- const config = {
- hostUrl: "https://rest-api.xfyun.cn/v2/itr",
- host: "rest-api.xfyun.cn",
- appid: Zotero.Prefs.get("Knowledge4Zotero.OCRXunfei.APPID"),
- apiSecret: Zotero.Prefs.get("Knowledge4Zotero.OCRXunfei.APISecret"),
- apiKey: Zotero.Prefs.get("Knowledge4Zotero.OCRXunfei.APIKey"),
- uri: "/v2/itr",
- };
-
- let date = new Date().toUTCString();
- let postBody = getPostBody();
- let digest = getDigest(postBody);
-
- const xhr = await Zotero.HTTP.request("POST", config.hostUrl, {
- headers: {
- "Content-Type": "application/json",
- Accept: "application/json,version=1.0",
- Host: config.host,
- Date: date,
- Digest: digest,
- Authorization: getAuthStr(date, digest),
- },
- body: JSON.stringify(postBody),
- responseType: "json",
- });
-
- if (xhr?.response?.code === 0) {
- result = xhr.response.data.region
- .filter((r) => r.type === "text")
- .map((r) => r.recog.content)
- .join(" ")
- .replace(/ifly-latex-(begin)?(end)?/g, "$");
- this._Addon.toolkit.Tool.log(xhr);
- success = true;
- } else {
- result =
- xhr.status === 200
- ? `${xhr.response.code} ${xhr.response.message}`
- : `${xhr.status} Error`;
- success = false;
- }
-
- function getPostBody() {
- let digestObj = {
- common: {
- app_id: config.appid,
- },
- business: {
- ent: "teach-photo-print",
- aue: "raw",
- },
- data: {
- image: src.split(",").pop(),
- },
- };
- return digestObj;
- }
-
- function getDigest(body) {
- return (
- "SHA-256=" +
- CryptoJS.enc.Base64.stringify(CryptoJS.SHA256(JSON.stringify(body)))
- );
- }
-
- function getAuthStr(date, digest) {
- let signatureOrigin = `host: ${config.host}\ndate: ${date}\nPOST ${config.uri} HTTP/1.1\ndigest: ${digest}`;
- let signatureSha = CryptoJS.HmacSHA256(
- signatureOrigin,
- config.apiSecret
- );
- let signature = CryptoJS.enc.Base64.stringify(signatureSha);
- let authorizationOrigin = `api_key="${config.apiKey}", algorithm="hmac-sha256", headers="host date request-line digest", signature="${signature}"`;
- return authorizationOrigin;
- }
- } else if (engine === "bing") {
- const xhr = await Zotero.HTTP.request(
- "POST",
- "https://www.bing.com/cameraexp/api/v1/getlatex",
- {
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- data: src.split(",").pop(),
- inputForm: "Image",
- clientInfo: { platform: "edge" },
- }),
- responseType: "json",
- }
- );
- if (xhr && xhr.status && xhr.status === 200 && !xhr.response.isError) {
- result = xhr.response.latex
- ? `$${xhr.response.latex}$`
- : xhr.response.ocrText;
- success = true;
- } else {
- result =
- xhr.status === 200
- ? xhr.response.errorMessage
- : `${xhr.status} Error`;
- success = false;
- }
- } else {
- result = "OCR Engine Not Found";
- success = false;
- }
- if (success) {
- annotationItem.annotationComment = `${
- annotationItem.annotationComment
- ? `${annotationItem.annotationComment}\n`
- : ""
- }${result}`;
- await annotationItem.saveTx();
- this._Addon.ZoteroViews.showProgressWindow(
- "Better Notes OCR",
- `OCR Result: ${result}`
- );
- } else {
- this._Addon.ZoteroViews.showProgressWindow(
- "Better Notes OCR",
- result,
- "fail"
- );
- }
- }
-}
-
-export default ReaderViews;
diff --git a/src/sync/syncController.ts b/src/sync/syncController.ts
deleted file mode 100644
index 83fa55a..0000000
--- a/src/sync/syncController.ts
+++ /dev/null
@@ -1,292 +0,0 @@
-/*
- * This file realizes the sycn feature.
- */
-
-import BetterNotes from "../addon";
-import AddonBase from "../module";
-import { SyncCode } from "../utils";
-
-class SyncController extends AddonBase {
- sycnLock: boolean;
-
- constructor(parent: BetterNotes) {
- super(parent);
- this.sycnLock = false;
- }
-
- getSyncNoteIds(): number[] {
- const ids = Zotero.Prefs.get("Knowledge4Zotero.syncNoteIds") as string;
- return Zotero.Items.get(ids.split(",").map((id: string) => Number(id)))
- .filter((item) => item.isNote())
- .map((item) => item.id);
- }
-
- isSyncNote(note: Zotero.Item): boolean {
- const syncNoteIds = this.getSyncNoteIds();
- return syncNoteIds.includes(note.id);
- }
-
- async getRelatedNoteIds(note: Zotero.Item): Promise {
- let allNoteIds: number[] = [note.id];
- const linkMatches = note.getNote().match(/zotero:\/\/note\/\w+\/\w+\//g);
- if (!linkMatches) {
- return allNoteIds;
- }
- const subNoteIds = (
- await Promise.all(
- linkMatches.map(async (link) =>
- this._Addon.NoteUtils.getNoteFromLink(link)
- )
- )
- )
- .filter((res) => res.item)
- .map((res) => res.item.id);
- allNoteIds = allNoteIds.concat(subNoteIds);
- allNoteIds = new Array(...new Set(allNoteIds));
- return allNoteIds;
- }
-
- async getRelatedNoteIdsFromNotes(notes: Zotero.Item[]): Promise {
- let allNoteIds: number[] = [];
- for (const note of notes) {
- allNoteIds = allNoteIds.concat(await this.getRelatedNoteIds(note));
- }
- return allNoteIds;
- }
-
- addSyncNote(noteItem: Zotero.Item) {
- const ids = this.getSyncNoteIds();
- if (ids.includes(noteItem.id)) {
- return;
- }
- ids.push(noteItem.id);
- Zotero.Prefs.set("Knowledge4Zotero.syncNoteIds", ids.join(","));
- }
-
- async removeSyncNote(noteItem: Zotero.Item) {
- const ids = this.getSyncNoteIds();
- Zotero.Prefs.set(
- "Knowledge4Zotero.syncNoteIds",
- ids.filter((id) => id !== noteItem.id).join(",")
- );
- Zotero.Prefs.clear(`Knowledge4Zotero.syncDetail-${noteItem.id}`);
- }
-
- async doCompare(noteItem: Zotero.Item): Promise {
- const syncStatus = this._Addon.SyncUtils.getSyncStatus(noteItem);
- const MDStatus = await this._Addon.SyncUtils.getMDStatus(noteItem);
- // No file found
- if (!MDStatus.meta) {
- return SyncCode.NoteAhead;
- }
- // File meta is unavailable
- if (MDStatus.meta.version < 0) {
- return SyncCode.NeedDiff;
- }
- let MDAhead = false;
- let noteAhead = false;
- const md5 = Zotero.Utilities.Internal.md5(MDStatus.content, false);
- const noteMd5 = Zotero.Utilities.Internal.md5(noteItem.getNote(), false);
- // MD5 doesn't match (md side change)
- if (md5 !== syncStatus.md5) {
- MDAhead = true;
- }
- // MD5 doesn't match (note side change)
- if (noteMd5 !== syncStatus.noteMd5) {
- noteAhead = true;
- }
- // Note version doesn't match (note side change)
- // This might be unreliable when Zotero account is not login
- if (Number(MDStatus.meta.version) !== noteItem.version) {
- noteAhead = true;
- }
- if (noteAhead && MDAhead) {
- return SyncCode.NeedDiff;
- } else if (noteAhead) {
- return SyncCode.NoteAhead;
- } else if (MDAhead) {
- return SyncCode.MDAhead;
- } else {
- return SyncCode.UpToDate;
- }
- }
-
- async updateNoteSyncStatus(noteItem: Zotero.Item, status: SyncStatus) {
- this.addSyncNote(noteItem);
- Zotero.Prefs.set(
- `Knowledge4Zotero.syncDetail-${noteItem.id}`,
- JSON.stringify(status)
- );
- }
-
- setSync() {
- const syncPeriod = Zotero.Prefs.get(
- "Knowledge4Zotero.syncPeriod"
- ) as number;
- if (syncPeriod > 0) {
- setInterval(() => {
- // Only when Zotero is active and focused
- if (document.hasFocus()) {
- this.doSync();
- }
- }, syncPeriod);
- }
- }
-
- async doSync(items: Zotero.Item[] = null, quiet: boolean = true) {
- if (this.sycnLock) {
- // Only allow one task
- return;
- }
- let progress;
- // Wrap the code in try...catch so that the lock can be released anyway
- try {
- this._Addon.toolkit.Tool.log("sync start");
- this.sycnLock = true;
- if (!items || !items.length) {
- items = Zotero.Items.get(this.getSyncNoteIds());
- }
- this._Addon.toolkit.Tool.log("BN:Sync", items);
-
- if (!quiet) {
- progress = this._Addon.ZoteroViews.showProgressWindow(
- "[Syncing] Better Notes",
- `[Check Status] 0/${items.length} ...`,
- "default",
- -1
- );
- progress.progress.setProgress(1);
- await this._Addon.ZoteroViews.waitProgressWindow(progress);
- }
- // Export items of same dir in batch
- const toExport = {};
- const toImport: SyncStatus[] = [];
- const toDiff: SyncStatus[] = [];
- let i = 1;
- for (const item of items) {
- const syncStatus = this._Addon.SyncUtils.getSyncStatus(item);
- const filepath = decodeURIComponent(syncStatus.path);
- let compareResult = await this.doCompare(item);
- switch (compareResult) {
- case SyncCode.NoteAhead:
- if (Object.keys(toExport).includes(filepath)) {
- toExport[filepath].push(item);
- } else {
- toExport[filepath] = [item];
- }
- break;
- case SyncCode.MDAhead:
- toImport.push(syncStatus);
- break;
- case SyncCode.NeedDiff:
- toDiff.push(syncStatus);
- break;
- default:
- break;
- }
- if (progress) {
- this._Addon.ZoteroViews.changeProgressWindowDescription(
- progress,
- `[Check Status] ${i}/${items.length} ...`
- );
- progress.progress.setProgress((i / items.length) * 100);
- }
- i += 1;
- }
- this._Addon.toolkit.Tool.log(toExport, toImport, toDiff);
- i = 1;
- let totalCount = Object.keys(toExport).length;
- for (const filepath of Object.keys(toExport)) {
- if (progress) {
- this._Addon.ZoteroViews.changeProgressWindowDescription(
- progress,
- `[Update MD] ${i}/${totalCount}, ${
- toImport.length + toDiff.length
- } queuing...`
- );
- progress.progress.setProgress(((i - 1) / totalCount) * 100);
- }
-
- await this._Addon.NoteExport.exportNotesToMDFiles(toExport[filepath], {
- useEmbed: false,
- useSync: true,
- filedir: filepath,
- withMeta: true,
- });
- i += 1;
- }
- i = 1;
- totalCount = toImport.length;
- for (const syncStatus of toImport) {
- if (progress) {
- this._Addon.ZoteroViews.changeProgressWindowDescription(
- progress,
- `[Update Note] ${i}/${totalCount}, ${toDiff.length} queuing...`
- );
- progress.progress.setProgress(((i - 1) / totalCount) * 100);
- }
- const item = Zotero.Items.get(syncStatus.itemID);
- const filepath = OS.Path.join(syncStatus.path, syncStatus.filename);
- await this._Addon.NoteImport.importMDFileToNote(filepath, item, {});
- await this._Addon.NoteExport.exportNotesToMDFiles([item], {
- useEmbed: false,
- useSync: true,
- filedir: syncStatus.path,
- withMeta: true,
- });
- i += 1;
- }
- i = 1;
- totalCount = toDiff.length;
- for (const syncStatus of toDiff) {
- if (progress) {
- this._Addon.ZoteroViews.changeProgressWindowDescription(
- progress,
- `[Compare Diff] ${i}/${totalCount}...`
- );
- progress.progress.setProgress(((i - 1) / totalCount) * 100);
- }
-
- const item = Zotero.Items.get(syncStatus.itemID);
- await this._Addon.SyncDiffWindow.doDiff(
- item,
- OS.Path.join(syncStatus.path, syncStatus.filename)
- );
- i += 1;
- }
- if (
- this._Addon.SyncInfoWindow._window &&
- !this._Addon.SyncInfoWindow._window.closed
- ) {
- this._Addon.SyncInfoWindow.doUpdate();
- }
- if (progress) {
- const syncCount =
- Object.keys(toExport).length + toImport.length + toDiff.length;
-
- this._Addon.ZoteroViews.changeProgressWindowDescription(
- progress,
- syncCount
- ? `[Finish] Sync ${syncCount} notes successfully`
- : "[Finish] Already up to date"
- );
- progress.progress.setProgress(100);
- progress.startCloseTimer(5000);
- }
- } catch (e) {
- this._Addon.toolkit.Tool.log(e);
- this._Addon.ZoteroViews.showProgressWindow(
- "[Syncing] Better Notes",
- String(e),
- "fail"
- );
- } finally {
- if (progress) {
- progress.startCloseTimer(5000);
- }
- }
- this.sycnLock = false;
- }
-}
-
-export default SyncController;
diff --git a/src/sync/syncDiffWindow.ts b/src/sync/syncDiffWindow.ts
deleted file mode 100644
index 33d3a20..0000000
--- a/src/sync/syncDiffWindow.ts
+++ /dev/null
@@ -1,162 +0,0 @@
-/*
- * This file realizes note diff with markdown file.
- */
-
-import BetterNotes from "../addon";
-import AddonBase from "../module";
-
-import { diffChars } from "diff";
-
-class SyncDiffWindow extends AddonBase {
- _window: any | Window;
- constructor(parent: BetterNotes) {
- super(parent);
- }
-
- async doDiff(noteItem: Zotero.Item, mdPath: string) {
- const syncStatus = this._Addon.SyncUtils.getSyncStatus(noteItem);
- const noteStatus = this._Addon.SyncUtils.getNoteStatus(noteItem);
- mdPath = Zotero.File.normalizeToUnix(mdPath);
- if (!noteItem || !noteItem.isNote() || !(await OS.File.exists(mdPath))) {
- return;
- }
- const mdStatus = await this._Addon.SyncUtils.getMDStatus(mdPath);
- if (!mdStatus.meta) {
- return;
- }
- const mdNoteContent = await this._Addon.NoteParse.parseMDToNote(
- mdStatus,
- noteItem,
- true
- );
- const noteContent = await this._Addon.NoteParse.parseNoteForDiff(noteItem);
- this._Addon.toolkit.Tool.log(mdNoteContent, noteContent);
- const changes = diffChars(noteContent, mdNoteContent);
- this._Addon.toolkit.Tool.log("changes", changes);
-
- const io = {
- defer: Zotero.Promise.defer(),
- result: "",
- type: "skip",
- };
-
- const syncDate = new Date(syncStatus.lastsync);
- if (!(noteStatus.lastmodify > syncDate && mdStatus.lastmodify > syncDate)) {
- // If only one kind of changes, merge automatically
- if (noteStatus.lastmodify >= mdStatus.lastmodify) {
- // refuse all, keep note
- io.result = changes
- .filter((diff) => (!diff.added && !diff.removed) || diff.removed)
- .map((diff) => diff.value)
- .join("");
- } else {
- // accept all, keep md
- io.result = changes
- .filter((diff) => (!diff.added && !diff.removed) || diff.added)
- .map((diff) => diff.value)
- .join("");
- }
- io.type = "finish";
- } else {
- // Otherwise, merge manually
- const imageAttachemnts = Zotero.Items.get(
- noteItem.getAttachments()
- ).filter((attch) => attch.isEmbeddedImageAttachment());
- const imageData = {};
- for (const image of imageAttachemnts) {
- try {
- const b64 = await this._Addon.SyncUtils._getDataURL(image);
- imageData[image.key] = b64;
- } catch (e) {
- this._Addon.toolkit.Tool.log(e);
- }
- }
-
- if (!this._window || this._window.closed) {
- this._window = window.open(
- "chrome://Knowledge4Zotero/content/diff.html",
- "betternotes-note-syncdiff",
- `chrome,centerscreen,resizable,status,width=900,height=550`
- );
- const defer = Zotero.Promise.defer();
- this._window.addEventListener("DOMContentLoaded", (e) => {
- defer.resolve();
- });
- // Incase we missed the content loaded event
- setTimeout(() => {
- if (this._window.document.readyState === "complete") {
- defer.resolve();
- }
- }, 500);
- await defer.promise;
- }
-
- this._window.document.title = `[Better Notes Sycing] Diff Merge of ${noteItem.getNoteTitle()}`;
- this._window.syncInfo = {
- noteName: noteItem.getNoteTitle(),
- noteModify: noteStatus.lastmodify.toISOString(),
- mdName: mdPath,
- mdModify: mdStatus.lastmodify.toISOString(),
- syncTime: syncDate.toISOString(),
- };
- this._window.diffData = changes.map((change, id) =>
- Object.assign(change, {
- id: id,
- text: change.value,
- })
- );
- this._window.imageData = imageData;
-
- this._window.io = io;
- this._window.initSyncInfo();
- this._window.initList();
- this._window.initDiffViewer();
- this._window.updateDiffRender([]);
- const abort = () => {
- this._Addon.toolkit.Tool.log("unloaded");
- io.defer.resolve();
- };
- // If closed by user, abort syncing
- this._window.addEventListener("beforeunload", abort);
- this._window.addEventListener("unload", abort);
- this._window.addEventListener("close", abort);
- this._window.onclose = abort;
- this._window.onbeforeunload = abort;
- this._window.onunload = abort;
- await io.defer.promise;
- }
-
- switch (io.type) {
- case "skip":
- alert(
- `Syncing of "${noteItem.getNoteTitle()}" is skipped.\nTo sync manually, go to File->Better Notes Sync Manager.`
- );
- this._window.closed || this._window.close();
- break;
- case "unsync":
- this._Addon.toolkit.Tool.log("remove synce", noteItem.getNote());
- await this._Addon.SyncController.removeSyncNote(noteItem);
- break;
- case "finish":
- this._Addon.toolkit.Tool.log("Diff result:", io.result);
- // return io.result;
- noteItem.setNote(noteStatus.meta + io.result + noteStatus.tail);
- await noteItem.saveTx({
- notifierData: {
- autoSyncDelay: Zotero.Notes.AUTO_SYNC_DELAY,
- },
- });
- await this._Addon.NoteExport.exportNotesToMDFiles([noteItem], {
- useEmbed: false,
- useSync: true,
- filedir: mdStatus.filedir,
- withMeta: true,
- });
- break;
- default:
- break;
- }
- }
-}
-
-export default SyncDiffWindow;
diff --git a/src/sync/syncInfoWindow.ts b/src/sync/syncInfoWindow.ts
deleted file mode 100644
index c148850..0000000
--- a/src/sync/syncInfoWindow.ts
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * This file contains sync info window related code.
- */
-
-import BetterNotes from "../addon";
-import AddonBase from "../module";
-
-class SyncInfoWindow extends AddonBase {
- triggerTime: number;
- public io: {
- dataIn: any;
- dataOut: any;
- deferred?: typeof Promise;
- };
- public _window: Window;
- constructor(parent: BetterNotes) {
- super(parent);
- }
-
- doLoad(_window: Window) {
- if (this._window && !this._window.closed) {
- this._window.close();
- }
- this._window = _window;
- this.io = (this._window as unknown as XUL.XULWindow).arguments[0];
- this.doUpdate();
- }
-
- doUpdate() {
- const syncInfo = this._Addon.SyncUtils.getSyncStatus(this.io.dataIn);
- const syncPathLable = this._window.document.getElementById(
- "Knowledge4Zotero-sync-path"
- );
- const path = `${decodeURIComponent(syncInfo.path)}/${decodeURIComponent(
- syncInfo.filename
- )}`;
-
- syncPathLable.setAttribute(
- "value",
- path.length > 50
- ? `${path.slice(0, 25)}...${path.slice(path.length - 25)}`
- : path
- );
- syncPathLable.setAttribute("tooltiptext", path);
-
- const copyCbk = (event) => {
- Zotero.Utilities.Internal.copyTextToClipboard(event.target.tooltipText);
- this._Addon.ZoteroViews.showProgressWindow(
- "Path Copied",
- event.target.tooltipText
- );
- };
- syncPathLable.removeEventListener("click", copyCbk);
- syncPathLable.addEventListener("click", copyCbk);
-
- let lastSync: string;
- const lastSyncTime = Number(syncInfo.lastsync);
- const currentTime = new Date().getTime();
- if (currentTime - lastSyncTime <= 60000) {
- lastSync = `${Math.round(
- (currentTime - lastSyncTime) / 1000
- )} seconds ago.`;
- } else if (currentTime - lastSyncTime <= 3600000) {
- lastSync = `${Math.round(
- (currentTime - lastSyncTime) / 60000
- )} minutes ago.`;
- } else {
- lastSync = new Date(lastSyncTime).toLocaleString();
- }
- this._window.document
- .getElementById("Knowledge4Zotero-sync-lastsync")
- .setAttribute("value", lastSync);
- setTimeout(() => {
- if (!this._window.closed) {
- this.doUpdate();
- }
- }, 3000);
- }
-
- doUnload() {
- this.io.deferred && this.io.deferred.resolve();
- }
-
- async doAccept() {
- // Update Settings
- let enable = (
- this._window.document.getElementById(
- "Knowledge4Zotero-sync-enable"
- ) as XUL.Checkbox
- ).checked;
- if (!enable) {
- const note = this.io.dataIn;
- const allNoteIds = await this._Addon.SyncController.getRelatedNoteIds(
- note
- );
- const notes = Zotero.Items.get(allNoteIds) as Zotero.Item[];
- for (const item of notes) {
- await this._Addon.SyncController.removeSyncNote(item);
- }
- this._Addon.ZoteroViews.showProgressWindow(
- "Better Notes",
- `Cancel sync of ${notes.length} notes.`
- );
- }
- }
- doExport() {
- this.io.dataOut.export = true;
- (this._window.document.querySelector("dialog") as any).acceptDialog();
- }
-}
-
-export default SyncInfoWindow;
diff --git a/src/sync/syncListWindow.ts b/src/sync/syncListWindow.ts
deleted file mode 100644
index 259861d..0000000
--- a/src/sync/syncListWindow.ts
+++ /dev/null
@@ -1,216 +0,0 @@
-/*
- * This file contains sync list window related code.
- */
-
-import BetterNotes from "../addon";
-import AddonBase from "../module";
-
-class SyncListWindow extends AddonBase {
- private _window: Window;
- constructor(parent: BetterNotes) {
- super(parent);
- }
-
- openSyncList() {
- if (this._window && !this._window.closed) {
- this._window.focus();
- this.doUpdate();
- } else {
- window.open(
- "chrome://Knowledge4Zotero/content/syncList.xul",
- "",
- "chrome,centerscreen,resizable,status,width=600,height=400"
- );
- }
- }
-
- doLoad(_window: Window) {
- this._window = _window;
- this.doUpdate();
- }
-
- doUpdate() {
- if (!this._window || this._window.closed) {
- return;
- }
- const notes = Zotero.Items.get(
- this._Addon.SyncController.getSyncNoteIds()
- ) as Zotero.Item[];
- const listbox = this._window.document.getElementById("sync-list");
- let e,
- es = this._window.document.getElementsByTagName("listitem");
- while (es.length > 0) {
- e = es[0];
- e.parentElement.removeChild(e);
- }
- for (const note of notes) {
- const syncInfo = this._Addon.SyncUtils.getSyncStatus(note);
- const listitem = this._Addon.toolkit.UI.createElement(
- this._window.document,
- "listitem",
- "xul"
- ) as XUL.ListItem;
- listitem.setAttribute("id", note.id);
-
- const icon = this._Addon.toolkit.UI.createElement(
- this._window.document,
- "listcell",
- "xul"
- ) as XUL.Element;
- icon.setAttribute("class", "listcell-iconic");
- icon.setAttribute("image", "chrome://zotero/skin/treeitem-note.png");
-
- const name = this._Addon.toolkit.UI.createElement(
- this._window.document,
- "listcell",
- "xul"
- ) as XUL.Element;
- name.setAttribute("label", `${note.getNoteTitle()}-${note.key}`);
-
- let lastSyncString: string;
- const lastSyncTime = Number(syncInfo.lastsync);
- const currentTime = new Date().getTime();
- if (currentTime - lastSyncTime <= 60000) {
- lastSyncString = `${Math.round(
- (currentTime - lastSyncTime) / 1000
- )} seconds ago.`;
- } else if (currentTime - lastSyncTime <= 3600000) {
- lastSyncString = `${Math.round(
- (currentTime - lastSyncTime) / 60000
- )} minutes ago.`;
- } else {
- lastSyncString = new Date(lastSyncTime).toLocaleString();
- }
- const lastSync = this._Addon.toolkit.UI.createElement(
- this._window.document,
- "listcell",
- "xul"
- ) as XUL.Element;
- lastSync.setAttribute("label", lastSyncString);
-
- const syncPath = this._Addon.toolkit.UI.createElement(
- this._window.document,
- "listcell",
- "xul"
- ) as XUL.Element;
- syncPath.setAttribute(
- "label",
- `${decodeURIComponent(syncInfo.path)}/${decodeURIComponent(
- syncInfo.filename
- )}`
- );
-
- listitem.append(icon, name, lastSync, syncPath);
-
- listitem.addEventListener("dblclick", (e) => {
- ZoteroPane.openNoteWindow(note.id);
- });
- listbox.append(listitem);
- }
-
- const periodButton = this._window.document.getElementById(
- "changesyncperiod"
- ) as XUL.Button;
- const period =
- Number(Zotero.Prefs.get("Knowledge4Zotero.syncPeriod")) / 1000;
- periodButton.setAttribute(
- "label",
- periodButton.getAttribute("label").split(":")[0] +
- ":" +
- (period > 0 ? period + "s" : "disabled")
- );
- this._window.focus();
- this.onSelect();
- }
-
- getSelectedItems(): Zotero.Item[] {
- return Zotero.Items.get(
- Array.prototype.map.call(
- (this._window.document.getElementById("sync-list") as any)
- .selectedItems,
- (node) => node.id
- ) as string[]
- );
- }
-
- onSelect() {
- const selected =
- (this._window.document.getElementById("sync-list") as any).selectedItems
- .length > 0;
- if (selected) {
- this._window.document
- .querySelector("#changesync")
- .removeAttribute("disabled");
- this._window.document
- .querySelector("#removesync")
- .removeAttribute("disabled");
- } else {
- this._window.document
- .querySelector("#changesync")
- .setAttribute("disabled", "true");
- this._window.document
- .querySelector("#removesync")
- .setAttribute("disabled", "true");
- }
- }
-
- useRelated(): Boolean {
- return confirm(
- "Apply changes to:\n[Yes] Selected note and it's linked notes\n[No] Only selected note"
- );
- }
-
- async doSync() {
- await this._Addon.SyncController.doSync(this.getSelectedItems(), false);
- this.doUpdate();
- }
-
- async changeSync() {
- const selectedItems = this.getSelectedItems();
- if (selectedItems.length === 0) {
- return;
- }
- await this._Addon.NoteExport.exportNotesToMDFiles(selectedItems, {
- useEmbed: false,
- useSync: true,
- withMeta: true,
- });
- this.doUpdate();
- }
-
- changeSyncPeriod(period: number = -1) {
- if (period < 0) {
- const inputPeriod = prompt("Enter synchronization period in seconds:");
- if (inputPeriod) {
- period = Number(inputPeriod);
- } else {
- return;
- }
- }
- if (period < 0) {
- period = 0;
- }
- Zotero.Prefs.set("Knowledge4Zotero.syncPeriod", Math.round(period) * 1000);
- this.doUpdate();
- }
-
- async removeSync() {
- let selectedItems = this.getSelectedItems();
- if (selectedItems.length === 0) {
- return;
- }
- if (this.useRelated()) {
- let noteIds: number[] =
- await this._Addon.SyncController.getRelatedNoteIdsFromNotes(
- selectedItems
- );
- selectedItems = Zotero.Items.get(noteIds) as Zotero.Item[];
- }
- for (const note of selectedItems) {
- await this._Addon.SyncController.removeSyncNote(note);
- }
- this.doUpdate();
- }
-}
-
-export default SyncListWindow;
diff --git a/src/sync/syncUtils.ts b/src/sync/syncUtils.ts
deleted file mode 100644
index c9c59c1..0000000
--- a/src/sync/syncUtils.ts
+++ /dev/null
@@ -1,1127 +0,0 @@
-import { unified } from "unified";
-import rehypeParse from "rehype-parse";
-import rehypeRemark from "rehype-remark";
-import remarkRehype from "remark-rehype";
-import rehypeStringify from "rehype-stringify";
-import remarkParse from "remark-parse";
-import remarkStringify from "remark-stringify";
-import { all, defaultHandlers } from "hast-util-to-mdast";
-import { toHtml } from "hast-util-to-html";
-import { toText } from "hast-util-to-text";
-import remarkGfm from "remark-gfm";
-import remarkMath from "remark-math";
-// visit may push nodes twice, use new Array(...new Set(nodes))
-// if the you want to process nodes outside visit
-import { visit } from "unist-util-visit";
-import { visitParents } from "unist-util-visit-parents";
-import rehypeFormat from "rehype-format";
-import { h } from "hastscript";
-import seedrandom = require("seedrandom");
-import YAML = require("yamljs");
-
-import BetterNotes from "../addon";
-import AddonBase from "../module";
-import { NodeMode } from "../utils";
-
-class SyncUtils extends AddonBase {
- constructor(parent: BetterNotes) {
- super(parent);
- }
-
- // A seedable version of Zotero.Utilities.randomString
- randomString(len: number, chars: string, seed: string) {
- if (!chars) {
- chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
- }
- if (!len) {
- len = 8;
- }
- let randomstring = "";
- const random: Function = seedrandom(seed);
- for (let i = 0; i < len; i++) {
- const rnum = Math.floor(random() * chars.length);
- randomstring += chars.substring(rnum, rnum + 1);
- }
- return randomstring;
- }
-
- async _getDataURL(item: Zotero.Item) {
- let path = (await item.getFilePathAsync()) as string;
- let buf = new Uint8Array((await OS.File.read(path, {})) as Uint8Array)
- .buffer;
- return (
- "data:" +
- item.attachmentContentType +
- ";base64," +
- this._arrayBufferToBase64(buf)
- );
- }
-
- _arrayBufferToBase64(buffer) {
- var binary = "";
- var bytes = new Uint8Array(buffer);
- var len = bytes.byteLength;
- for (var i = 0; i < len; i++) {
- binary += String.fromCharCode(bytes[i]);
- }
- return btoa(binary);
- }
-
- getNoteStatus(noteItem: Zotero.Item): NoteStatus {
- if (!noteItem.isNote()) {
- return;
- }
- const fullContent = noteItem.getNote();
- const ret = {
- meta: "",
- content: "",
- tail: "",
- lastmodify: Zotero.Date.sqlToDate(noteItem.dateModified, true),
- };
- const metaRegex = /"?data-schema-version"?="[0-9]*">/;
- const match = fullContent?.match(metaRegex);
- if (!match || match.length == 0) {
- ret.meta = '';
- ret.content = fullContent || "";
- return ret;
- }
- const idx = fullContent.search(metaRegex);
- if (idx != -1) {
- ret.content = fullContent.substring(
- idx + match[0].length,
- fullContent.length - ret.tail.length
- );
- }
- return ret;
- }
-
- getEmptySyncStatus(): SyncStatus {
- return {
- path: "",
- filename: "",
- md5: "",
- noteMd5: "",
- lastsync: new Date().getTime(),
- itemID: -1,
- };
- }
-
- getSyncStatus(noteItem: Zotero.Item): SyncStatus {
- return JSON.parse(
- (Zotero.Prefs.get(
- `Knowledge4Zotero.syncDetail-${noteItem.id}`
- ) as string) || JSON.stringify(this.getEmptySyncStatus())
- );
- }
-
- getMDStatusFromContent(contentRaw: string): MDStatus {
- const result = contentRaw.match(/^---([\s\S]*)---\n/);
- 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) {
- this._Addon.toolkit.Tool.log(e);
- }
- }
- return ret;
- }
-
- async getMDStatus(source: Zotero.Item | 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 (source.isNote && source.isNote()) {
- const syncStatus = this.getSyncStatus(source);
- filepath = `${syncStatus.path}/${syncStatus.filename}`;
- }
- filepath = Zotero.File.normalizeToUnix(filepath);
- if (await OS.File.exists(filepath)) {
- let contentRaw = (await OS.File.read(filepath, {
- encoding: "utf-8",
- })) as string;
- ret = this.getMDStatusFromContent(contentRaw);
- const pathSplit = filepath.split("/");
- ret.filedir = Zotero.File.normalizeToUnix(
- pathSplit.slice(0, -1).join("/")
- );
- ret.filename = filepath.split("/").pop();
- const stat = await OS.File.stat(filepath);
- ret.lastmodify = stat.lastModificationDate;
- }
- } catch (e) {
- this._Addon.toolkit.Tool.log(e);
- }
- return ret;
- }
-
- note2rehype(str) {
- const rehype = unified()
- .use(remarkGfm)
- .use(remarkMath)
- .use(rehypeParse, { fragment: true })
- .parse(str);
-
- // Make sure is inline break. Remove \n before/after
- const removeBlank = (node, parentNode, offset) => {
- const idx = parentNode.children.indexOf(node);
- const target = parentNode.children[idx + offset];
- if (
- target &&
- target.type === "text" &&
- !target.value.replace(/[\r\n]/g, "")
- ) {
- (parentNode.children as any[]).splice(idx + offset, 1);
- }
- };
- visitParents(
- rehype,
- (_n: any) => _n.type === "element" && _n.tagName === "br",
- (_n: any, ancestors) => {
- if (ancestors.length) {
- const parentNode = ancestors[ancestors.length - 1];
- removeBlank(_n, parentNode, -1);
- removeBlank(_n, parentNode, 1);
- }
- }
- );
-
- // Make sure and wrapped by
- visitParents(
- rehype,
- (_n: any) =>
- _n.type === "element" &&
- (_n.tagName === "span" || _n.tagName === "img"),
- (_n: any, ancestors) => {
- if (ancestors.length) {
- const parentNode = ancestors[ancestors.length - 1];
- if (parentNode === rehype) {
- const newChild = h("span");
- this.replace(newChild, _n);
- const p = h("p", [newChild]);
- this.replace(_n, p);
- }
- }
- }
- );
-
- // Make sure empty
under root node is removed
- visitParents(
- rehype,
- (_n: any) => _n.type === "element" && _n.tagName === "p",
- (_n: any, ancestors) => {
- if (ancestors.length) {
- const parentNode = ancestors[ancestors.length - 1];
- if (parentNode === rehype && !_n.children.length && !toText(_n)) {
- parentNode.children.splice(parentNode.children.indexOf(_n), 1);
- }
- }
- }
- );
- return rehype;
- }
-
- async rehype2remark(rehype) {
- return await unified()
- .use(rehypeRemark, {
- handlers: {
- span: (h, node) => {
- if (
- node.properties?.style?.includes("text-decoration: line-through")
- ) {
- return h(node, "delete", all(h, node));
- } else if (node.properties?.style?.includes("background-color")) {
- return h(node, "html", toHtml(node));
- } else if (node.properties?.className?.includes("math")) {
- return h(node, "inlineMath", toText(node).slice(1, -1));
- } else {
- return h(node, "paragraph", all(h, node));
- }
- },
- pre: (h, node) => {
- if (node.properties?.className?.includes("math")) {
- return h(node, "math", toText(node).slice(2, -2));
- } else {
- return h(node, "code", toText(node));
- }
- },
- u: (h, node) => {
- return h(node, "u", toText(node));
- },
- sub: (h, node) => {
- return h(node, "sub", toText(node));
- },
- sup: (h, node) => {
- return h(node, "sup", toText(node));
- },
- table: (h, node) => {
- let hasStyle = false;
- visit(
- node,
- (_n) =>
- _n.type === "element" &&
- ["tr", "td", "th"].includes((_n as any).tagName),
- (node) => {
- if (node.properties.style) {
- hasStyle = true;
- }
- }
- );
- if (0 && hasStyle) {
- return h(node, "styleTable", toHtml(node));
- } else {
- return defaultHandlers.table(h, node);
- }
- },
- wrapper: (h, node) => {
- return h(node, "wrapper", toText(node));
- },
- wrapperleft: (h, node) => {
- return h(node, "wrapperleft", toText(node));
- },
- wrapperright: (h, node) => {
- return h(node, "wrapperright", toText(node));
- },
- zhighlight: (h, node) => {
- return h(node, "zhighlight", toHtml(node));
- },
- zcitation: (h, node) => {
- return h(node, "zcitation", toHtml(node));
- },
- znotelink: (h, node) => {
- return h(node, "znotelink", toHtml(node));
- },
- zimage: (h, node) => {
- return h(node, "zimage", toHtml(node));
- },
- },
- })
- .run(rehype);
- }
-
- remark2md(remark) {
- return String(
- unified()
- .use(remarkGfm)
- .use(remarkMath)
- .use(remarkStringify, {
- handlers: {
- pre: (node) => {
- return "```\n" + node.value + "\n```";
- },
- u: (node) => {
- return "" + node.value + " ";
- },
- sub: (node) => {
- return "" + node.value + " ";
- },
- sup: (node) => {
- return "" + node.value + " ";
- },
- styleTable: (node) => {
- return node.value;
- },
- wrapper: (node) => {
- return "\n\n";
- },
- wrapperleft: (node) => {
- return "\n";
- },
- wrapperright: (node) => {
- return "\n";
- },
- zhighlight: (node) => {
- return node.value.replace(/(^|<\/zhighlight>$)/g, "");
- },
- zcitation: (node) => {
- return node.value.replace(/(^|<\/zcitation>$)/g, "");
- },
- znotelink: (node) => {
- return node.value.replace(/(^|<\/znotelink>$)/g, "");
- },
- zimage: (node) => {
- return node.value.replace(/(^|<\/zimage>$)/g, "");
- },
- },
- })
- .stringify(remark)
- );
- }
-
- md2remark(str) {
- // Parse Obsidian-style image ![[xxx.png]]
- // Encode spaces in link, otherwise it cannot be parsed to image node
- str = str
- .replace(/!\[\[(.*)\]\]/g, (s: string) => `})`)
- .replace(
- /!\[.*\]\((.*)\)/g,
- (s: string) =>
- `/g)[0].slice(1, -1))})`
- );
- const remark = unified()
- .use(remarkGfm)
- .use(remarkMath)
- .use(remarkParse)
- .parse(str);
- // visit(
- // remark,
- // (_n) => _n.type === "image",
- // (_n: any) => {
- // _n.type = "html";
- // _n.value = toHtml(
- // h("img", {
- // src: _n.url,
- // })
- // );
- // }
- // );
- return remark;
- }
-
- async remark2rehype(remark) {
- return await unified()
- .use(remarkRehype, {
- allowDangerousHtml: true,
- })
- .run(remark);
- }
-
- rehype2note(rehype) {
- // Del node
- visit(
- rehype,
- (node) => node.type === "element" && (node as any).tagName === "del",
- (node) => {
- node.tagName = "span";
- node.properties.style = "text-decoration: line-through";
- }
- );
-
- // Code node
- visitParents(
- rehype,
- (node) => node.type === "element" && (node as any).tagName === "code",
- (node, ancestors) => {
- const parent = ancestors.length
- ? ancestors[ancestors.length - 1]
- : undefined;
- if (parent?.type == "element" && parent?.tagName === "pre") {
- node.value = toText(node);
- node.type = "text";
- }
- }
- );
-
- // Table node with style
- visit(
- rehype,
- (node) => node.type === "element" && (node as any).tagName === "table",
- (node) => {
- let hasStyle = false;
- visit(
- node,
- (_n) =>
- _n.type === "element" &&
- ["tr", "td", "th"].includes((_n as any).tagName),
- (node) => {
- if (node.properties.style) {
- hasStyle = true;
- }
- }
- );
- if (hasStyle) {
- node.value = toHtml(node).replace(/[\r\n]/g, "");
- node.children = [];
- node.type = "raw";
- }
- }
- );
-
- // Convert thead to tbody
- visit(
- rehype,
- (node) => node.type === "element" && (node as any).tagName === "thead",
- (node) => {
- node.value = toHtml(node).slice(7, -8);
- node.children = [];
- node.type = "raw";
- }
- );
-
- // Wrap lines in list with (for diff)
- visitParents(rehype, "text", (node, ancestors) => {
- const parent = ancestors.length
- ? ancestors[ancestors.length - 1]
- : undefined;
- if (
- node.value.replace(/[\r\n]/g, "") &&
- parent?.type == "element" &&
- ["li", "td"].includes(parent?.tagName)
- ) {
- node.type = "element";
- node.tagName = "p";
- node.children = [
- { type: "text", value: node.value.replace(/[\r\n]/g, "") },
- ];
- node.value = undefined;
- }
- });
-
- // No empty breakline text node in list (for diff)
- visit(
- rehype,
- (node) =>
- node.type === "element" &&
- ((node as any).tagName === "li" || (node as any).tagName === "td"),
- (node) => {
- node.children = node.children.filter(
- (_n) =>
- _n.type === "element" ||
- (_n.type === "text" && _n.value.replace(/[\r\n]/g, ""))
- );
- }
- );
-
- // Math node
- visit(
- rehype,
- (node) =>
- node.type === "element" &&
- ((node as any).properties?.className?.includes("math-inline") ||
- (node as any).properties?.className?.includes("math-display")),
- (node) => {
- if (node.properties.className.includes("math-inline")) {
- node.children = [
- { type: "text", value: "$" },
- ...node.children,
- { type: "text", value: "$" },
- ];
- } else if (node.properties.className.includes("math-display")) {
- node.children = [
- { type: "text", value: "$$" },
- ...node.children,
- { type: "text", value: "$$" },
- ];
- node.tagName = "pre";
- }
- node.properties.className = "math";
- }
- );
-
- // Ignore link rel attribute, which exists in note
- visit(
- rehype,
- (node) => node.type === "element" && (node as any).tagName === "a",
- (node) => {
- node.properties.rel = undefined;
- }
- );
-
- // Ignore empty lines, as they are not parsed to md
- const tempChildren = [];
- const isEmptyNode = (_n) =>
- (_n.type === "text" && !_n.value.trim()) ||
- (_n.type === "element" &&
- _n.tagName === "p" &&
- !_n.children.length &&
- !toText(_n).trim());
- for (const child of rehype.children) {
- if (
- tempChildren.length &&
- isEmptyNode(tempChildren[tempChildren.length - 1]) &&
- isEmptyNode(child)
- ) {
- continue;
- }
- tempChildren.push(child);
- }
-
- rehype.children = tempChildren;
-
- this._Addon.toolkit.Tool.log("before n2r", rehype);
-
- return unified()
- .use(rehypeStringify, {
- allowDangerousCharacters: true,
- allowDangerousHtml: true,
- })
- .stringify(rehype);
- }
-
- async rehype2rehype(rehype) {
- return await unified().use(rehypeFormat).run(rehype);
- }
-
- async note2md(str) {
- const rehype = this.note2rehype(str);
- const remark = await this.rehype2remark(rehype);
- const md = this.remark2md(remark);
- return md;
- }
-
- async md2note(str) {
- const remark = this.md2remark(str);
- this._Addon.toolkit.Tool.log(remark);
- let rehype = await this.remark2rehype(remark);
- this._Addon.toolkit.Tool.log(rehype);
- const html = this.rehype2note(rehype);
- this._Addon.toolkit.Tool.log(html);
- return html;
- }
-
- async note2note(str) {
- let rehype = this.note2rehype(str);
- const html = this.rehype2note(rehype);
- return html;
- }
-
- replace(targetNode, sourceNode) {
- targetNode.type = sourceNode.type;
- targetNode.tagName = sourceNode.tagName;
- targetNode.properties = sourceNode.properties;
- targetNode.value = sourceNode.value;
- targetNode.children = sourceNode.children;
- }
-
- getN2MRehypeHighlightNodes(rehype) {
- const nodes = [];
- visit(
- rehype,
- (node: any) =>
- node.type === "element" &&
- node.properties?.className?.includes("highlight"),
- (node) => nodes.push(node)
- );
- return new Array(...new Set(nodes));
- }
-
- getN2MRehypeCitationNodes(rehype) {
- const nodes = [];
- visit(
- rehype,
- (node: any) =>
- node.type === "element" &&
- node.properties?.className?.includes("citation"),
- (node) => nodes.push(node)
- );
- return new Array(...new Set(nodes));
- }
-
- getN2MRehypeNoteLinkNodes(rehype) {
- const nodes = [];
- visit(
- rehype,
- (node: any) =>
- node.type === "element" &&
- node.tagName === "a" &&
- node.properties?.href &&
- /zotero:\/\/note\/\w+\/\w+\//.test(node.properties?.href),
- (node) => nodes.push(node)
- );
- this._Addon.toolkit.Tool.log("N2M link");
- this._Addon.toolkit.Tool.log(JSON.stringify(nodes));
- return new Array(...new Set(nodes));
- }
-
- getN2MRehypeImageNodes(rehype) {
- const nodes = [];
- visit(
- rehype,
- (node: any) =>
- node.type === "element" &&
- node.tagName === "img" &&
- node.properties?.dataAttachmentKey,
- (node) => nodes.push(node)
- );
- return new Array(...new Set(nodes));
- }
-
- processN2MRehypeHighlightNodes(nodes, mode: NodeMode = NodeMode.default) {
- if (!nodes.length) {
- return;
- }
- for (const node of nodes) {
- let annotation;
- try {
- annotation = JSON.parse(
- decodeURIComponent(node.properties.dataAnnotation)
- );
- } catch (e) {
- continue;
- }
- if (!annotation) {
- continue;
- }
- // annotation.uri was used before note-editor v4
- let uri = annotation.attachmentURI || annotation.uri;
- let position = annotation.position;
- this._Addon.toolkit.Tool.log("----Debug Link----");
- this._Addon.toolkit.Tool.log(annotation);
- this._Addon.toolkit.Tool.log("convertAnnotations", node);
-
- if (typeof uri === "string" && typeof position === "object") {
- this._Addon.toolkit.Tool.log(uri);
- let openURI;
- let uriParts = uri.split("/");
- let libraryType = uriParts[3];
- let key = uriParts[uriParts.length - 1];
- this._Addon.toolkit.Tool.log(key);
- if (libraryType === "users") {
- openURI = "zotero://open-pdf/library/items/" + key;
- }
- // groups
- else {
- let groupID = uriParts[4];
- openURI = "zotero://open-pdf/groups/" + groupID + "/items/" + key;
- }
-
- openURI +=
- "?page=" +
- (position.pageIndex + 1) +
- (annotation.annotationKey
- ? "&annotation=" + annotation.annotationKey
- : "");
-
- let newNode = h("span", [
- h(node.tagName, node.properties, node.children),
- h("span", " ("),
- h("a", { href: openURI }, ["pdf"]),
- h("span", ") "),
- ]);
- const annotKey =
- annotation.annotationKey ||
- this.randomString(
- 8,
- Zotero.Utilities.allowedKeyChars,
- Zotero.Utilities.Internal.md5(node.properties.dataAnnotation)
- );
-
- if (mode === NodeMode.wrap) {
- newNode.children.splice(0, 0, h("wrapperleft", `annot:${annotKey}`));
- newNode.children.push(h("wrapperright", `annot:${annotKey}`));
- } else if (mode === NodeMode.replace) {
- newNode = h("placeholder", `annot:${annotKey}`);
- } else if (mode === NodeMode.direct) {
- const newChild = h("span");
- this.replace(newChild, node);
- newChild.children = [h("a", { href: openURI }, node.children)];
- newChild.properties.ztype = "zhighlight";
- newNode = h("zhighlight", [newChild]);
- }
- this._Addon.toolkit.Tool.log(newNode, node);
- this.replace(node, newNode);
- this._Addon.toolkit.Tool.log("converted", newNode, node);
- }
- }
- }
-
- processN2MRehypeCitationNodes(nodes, mode: NodeMode = NodeMode.default) {
- if (!nodes.length) {
- return;
- }
- for (const node of nodes) {
- let citation;
- try {
- citation = JSON.parse(decodeURIComponent(node.properties.dataCitation));
- } catch (e) {
- continue;
- }
- if (!citation?.citationItems?.length) {
- continue;
- }
-
- let uris = [];
- for (let citationItem of citation.citationItems) {
- let uri = citationItem.uris[0];
- if (typeof uri === "string") {
- let uriParts = uri.split("/");
- let libraryType = uriParts[3];
- let key = uriParts[uriParts.length - 1];
- this._Addon.toolkit.Tool.log(key);
- if (libraryType === "users") {
- uris.push("zotero://select/library/items/" + key);
- }
- // groups
- else {
- let groupID = uriParts[4];
- uris.push("zotero://select/groups/" + groupID + "/items/" + key);
- }
- }
- }
-
- let childNodes = [];
-
- visit(
- node,
- (_n: any) => _n.properties?.className.includes("citation-item"),
- (_n) => {
- return childNodes.push(_n);
- }
- );
-
- // For unknown reasons, the element will be duplicated. Remove them.
- childNodes = new Array(...new Set(childNodes));
-
- // Fallback to pre v5 note-editor schema that was serializing citations as plain text i.e.:
- // (Jang et al., 2005, p. 14; Kongsgaard et al., 2009, p. 790)
- if (!childNodes.length) {
- childNodes = toText(node).slice(1, -1).split("; ");
- }
-
- let newNode = h("span", node.properties, [
- { type: "text", value: "(" },
- ...childNodes.map((child, i) => {
- const newNode = h("span");
- this.replace(newNode, child);
- this._Addon.toolkit.Tool.log("cite child", child, newNode);
- newNode.children = [h("a", { href: uris[i] }, child.children)];
- return newNode;
- }),
- { type: "text", value: ")" },
- ]);
- this._Addon.toolkit.Tool.log("cite", newNode);
- const citationKey = this.randomString(
- 8,
- Zotero.Utilities.allowedKeyChars,
- Zotero.Utilities.Internal.md5(node.properties.dataCitation)
- );
- if (mode === NodeMode.wrap) {
- newNode.children.splice(0, 0, h("wrapperleft", `cite:${citationKey}`));
- newNode.children.push(h("wrapperright", `cite:${citationKey}`));
- } else if (mode === NodeMode.replace) {
- newNode = h("placeholder", `cite:${citationKey}`);
- } else if (mode === NodeMode.direct) {
- const newChild = h("span");
- this.replace(newChild, newNode);
- newChild.properties.ztype = "zcitation";
- newNode = h("zcitation", [newChild]);
- }
- this.replace(node, newNode);
- }
- }
-
- processN2MRehypeNoteLinkNodes(
- nodes,
- infoList: Array<{
- link: string;
- id: number;
- note: Zotero.Item;
- filename: string;
- }>,
- mode: NodeMode = NodeMode.default
- ) {
- if (!nodes.length) {
- return;
- }
- for (const node of nodes) {
- this._Addon.toolkit.Tool.log("note link", node);
- const noteInfo = infoList.find((i) =>
- node.properties.href.includes(i.link)
- );
- if (!noteInfo) {
- continue;
- }
- const link = `./${noteInfo.filename}`;
- const linkKey = this.randomString(
- 8,
- Zotero.Utilities.allowedKeyChars,
- Zotero.Utilities.Internal.md5(node.properties.href)
- );
- if (mode === NodeMode.wrap) {
- const newNode = h("span", [
- h("wrapperleft", `note:${linkKey}`),
- h(
- node.tagName,
- Object.assign(node.properties, { href: link }),
- node.children
- ),
- h("wrapperright", `note:${linkKey}`),
- ]);
- this.replace(node, newNode);
- } else if (mode === NodeMode.replace) {
- const newNode = h("placeholder", `note:${linkKey}`);
- this.replace(node, newNode);
- } else if (mode === NodeMode.direct) {
- const newChild = h("a", node.properties, node.children);
- newChild.properties.zhref = node.properties.href;
- newChild.properties.href = link;
- newChild.properties.ztype = "znotelink";
- newChild.properties.class = "internal-link"; // required for obsidian compatibility
- const newNode = h("znotelink", [newChild]);
- this.replace(node, newNode);
- this._Addon.toolkit.Tool.log("direct link", node, newNode, newChild);
- }
- this._Addon.toolkit.Tool.log("note link parsed", node);
- }
- }
-
- async processN2MRehypeImageNodes(
- nodes,
- libraryID: number,
- Path: string,
- skipSavingImages: boolean = false,
- absolutePath: boolean = false,
- mode: NodeMode = NodeMode.default
- ) {
- if (!nodes.length) {
- return;
- }
- for (const node of nodes) {
- let imgKey = node.properties.dataAttachmentKey;
-
- const attachmentItem = await Zotero.Items.getByLibraryAndKeyAsync(
- libraryID,
- imgKey
- );
- this._Addon.toolkit.Tool.log(attachmentItem);
- this._Addon.toolkit.Tool.log(
- "image",
- libraryID,
- imgKey,
- attachmentItem,
- node
- );
- if (!attachmentItem) {
- continue;
- }
-
- let oldFile = String(await attachmentItem.getFilePathAsync());
- this._Addon.toolkit.Tool.log(oldFile);
- let ext = oldFile.split(".").pop();
- let newAbsPath = Zotero.BetterNotes.NoteUtils.formatPath(
- `${Path}/${imgKey}.${ext}`
- );
- this._Addon.toolkit.Tool.log(newAbsPath);
- let newFile = oldFile;
- try {
- // Don't overwrite
- if (skipSavingImages || (await OS.File.exists(newAbsPath))) {
- newFile = newAbsPath.replace(/\\/g, "/");
- } else {
- newFile = (await Zotero.File.copyToUnique(oldFile, newAbsPath)).path;
- newFile = newFile.replace(/\\/g, "/");
- }
- newFile = Zotero.File.normalizeToUnix(
- absolutePath ? newFile : `attachments/${newFile.split(/\//).pop()}`
- );
- } catch (e) {
- this._Addon.toolkit.Tool.log(e);
- }
- this._Addon.toolkit.Tool.log(newFile);
-
- node.properties.src = newFile ? newFile : oldFile;
-
- if (mode === NodeMode.direct) {
- const newChild = h("span");
- this.replace(newChild, node);
- newChild.properties.ztype = "zimage";
- // const newNode = h("zimage", [newChild]);
- // this.replace(node, newNode);
- node.properties.alt = toHtml(newChild);
- }
- this._Addon.toolkit.Tool.log("zimage", node);
- }
- }
-
- getM2NRehypeAnnotationNodes(rehype) {
- const nodes = [];
- visit(
- rehype,
- (node: any) => node.type === "element" && node.properties?.dataAnnotation,
- (node) => nodes.push(node)
- );
- return new Array(...new Set(nodes));
- }
-
- getM2NRehypeHighlightNodes(rehype) {
- const nodes = [];
- visit(
- rehype,
- (node: any) =>
- node.type === "element" && node.properties?.ztype === "zhighlight",
- (node) => nodes.push(node)
- );
- this._Addon.toolkit.Tool.log("N2M:highlight", nodes);
- return new Array(...new Set(nodes));
- }
-
- getM2NRehypeCitationNodes(rehype) {
- const nodes = [];
- visit(
- rehype,
- (node: any) =>
- node.type === "element" &&
- (node.properties?.ztype === "zcitation" ||
- node.properties?.dataCitation),
- (node) => nodes.push(node)
- );
- return new Array(...new Set(nodes));
- }
-
- getM2NRehypeNoteLinkNodes(rehype) {
- const nodes = [];
- visit(
- rehype,
- (node: any) =>
- node.type === "element" && node.properties?.ztype === "znotelink",
- (node) => nodes.push(node)
- );
- return new Array(...new Set(nodes));
- }
-
- getM2NRehypeImageNodes(rehype) {
- const nodes = [];
- visit(
- rehype,
- (node: any) => node.type === "element" && node.tagName === "img",
- (node) => nodes.push(node)
- );
- return new Array(...new Set(nodes));
- }
-
- processM2NRehypeMetaImageNodes(nodes) {
- if (!nodes.length) {
- return;
- }
-
- this._Addon.toolkit.Tool.log("processing M2N meta images", nodes);
- for (const node of nodes) {
- if (/zimage/.test(node.properties.alt)) {
- const newNode: any = unified()
- .use(remarkGfm)
- .use(remarkMath)
- .use(rehypeParse, { fragment: true })
- .parse(node.properties.alt);
- this._Addon.toolkit.Tool.log(newNode);
- newNode.properties.src = node.properties.src;
- this.replace(node, newNode);
- }
- }
- }
-
- processM2NRehypeHighlightNodes(nodes) {
- if (!nodes.length) {
- return;
- }
- for (const node of nodes) {
- // node.children[0] is , its children is the real children
- node.children = node.children[0].children;
- delete node.properties.ztype;
- }
- }
-
- async processM2NRehypeCitationNodes(nodes, isImport: boolean = false) {
- if (!nodes.length) {
- return;
- }
- for (const node of nodes) {
- if (isImport) {
- try {
- // {
- // "citationItems": [
- // {
- // "uris": [
- // "http://zotero.org/users/uid/items/itemkey"
- // ]
- // }
- // ],
- // "properties": {}
- // }
- const dataCitation = JSON.parse(
- decodeURIComponent(node.properties.dataCitation)
- );
- const ids = dataCitation.citationItems.map((c) =>
- Zotero.URI.getURIItemID(c.uris[0])
- );
- const html = await this._Addon.NoteParse.parseCitationHTML(ids);
- const newNode = this.note2rehype(html);
- // root -> p -> span(cite, this is what we actually want)
- this.replace(node, (newNode.children[0] as any).children[0]);
- } catch (e) {
- this._Addon.toolkit.Tool.log(e);
- continue;
- }
- } else {
- visit(
- node,
- (_n: any) => _n.properties?.className.includes("citation-item"),
- (_n) => {
- _n.children = [{ type: "text", value: toText(_n) }];
- }
- );
- delete node.properties?.ztype;
- }
- }
- }
-
- processM2NRehypeNoteLinkNodes(nodes) {
- if (!nodes.length) {
- return;
- }
- for (const node of nodes) {
- node.properties.href = node.properties.zhref;
- delete node.properties.class;
- delete node.properties.zhref;
- delete node.properties.ztype;
- }
- }
-
- async processM2NRehypeImageNodes(
- nodes: any[],
- noteItem: Zotero.Item,
- fileDir: string,
- isImport: boolean = false
- ) {
- if (!nodes.length || (isImport && !noteItem)) {
- return;
- }
-
- this._Addon.toolkit.Tool.log("processing M2N images", nodes);
- for (const node of nodes) {
- if (isImport) {
- // We encode the src in md2remark and decode it here.
- let src = Zotero.File.normalizeToUnix(
- decodeURIComponent(node.properties.src)
- );
- const srcType = (src as string).startsWith("data:")
- ? "b64"
- : (src as string).startsWith("http")
- ? "url"
- : "file";
- if (srcType === "file") {
- if (!(await OS.File.exists(src))) {
- src = OS.Path.join(fileDir, src);
- if (!(await OS.File.exists(src))) {
- this._Addon.toolkit.Tool.log("parse image, path invalid");
- continue;
- }
- }
- }
- const key = await (
- Zotero.BetterNotes as BetterNotes
- ).NoteUtils.importImageToNote(noteItem, src, srcType);
- node.properties.dataAttachmentKey = key;
- }
- delete node.properties.src;
- node.properties.ztype && delete node.properties.ztype;
- }
- }
-}
-
-export { SyncUtils, NodeMode };
diff --git a/src/template/templateController.ts b/src/template/templateController.ts
deleted file mode 100644
index bc32b49..0000000
--- a/src/template/templateController.ts
+++ /dev/null
@@ -1,286 +0,0 @@
-/*
- * This file realizes the template feature.
- */
-
-import BetterNotes from "../addon";
-import { NoteTemplate } from "../utils";
-import AddonBase from "../module";
-
-class TemplateController extends AddonBase {
- _systemTemplateNames: string[];
- _defaultTemplates: NoteTemplate[];
- constructor(parent: BetterNotes) {
- super(parent);
- this._systemTemplateNames = [
- "[QuickInsert]",
- "[QuickBackLink]",
- "[QuickImport]",
- "[QuickNoteV4]",
- "[ExportMDFileName]",
- "[ExportMDFileHeader]",
- ];
- this._defaultTemplates = [
- {
- name: "[QuickInsert]",
- text: '${subNoteItem.getNoteTitle().trim() ? subNoteItem.getNoteTitle().trim() : link}
',
- disabled: false,
- },
- {
- name: "[QuickBackLink]",
- text: 'Referred in ${noteItem.getNoteTitle().trim() ? noteItem.getNoteTitle().trim() : "Main Note"}${sectionName?`/${sectionName}`:""}
',
- disabled: false,
- },
- {
- name: "[QuickImport]",
- text: '\nLinked Note:
\n${subNoteLines.join("")}\n ',
- disabled: false,
- },
- {
- name: "[QuickNoteV4]",
- text: '${await new Promise(async (r) => {\nlet res = ""\nif(annotationItem.annotationComment){\nres += await Zotero.BetterNotes.NoteParse.parseMDToHTML(annotationItem.annotationComment);\n}\nres += await Zotero.BetterNotes.NoteParse.parseAnnotationHTML(noteItem, [annotationItem], true);\nr(res);})}',
- disabled: false,
- },
- {
- name: "[ExportMDFileName]",
- text: '${(noteItem.getNoteTitle ? noteItem.getNoteTitle().replace(/[/\\\\?%*:|"<> ]/g, "-") + "-" : "")}${noteItem.key}.md',
- disabled: false,
- },
- {
- name: "[ExportMDFileHeader]",
- text: '${await new Promise(async (r) => {\n let header = {};\n header.tags = noteItem.getTags().map((_t) => _t.tag);\n header.parent = noteItem.parentItem? noteItem.parentItem.getField("title") : "";\n header.collections = (\n await Zotero.Collections.getCollectionsContainingItems([\n (noteItem.parentItem || noteItem).id,\n ])\n ).map((c) => c.name);\n r(JSON.stringify(header));\n})}\n',
- disabled: false,
- },
- {
- name: "[Item] item-notes with metadata",
- text: '${topItem.getField("title")} \n💡 Meta Data \n
\n \n \n Title
\n \n \n ${topItem.getField(\'title\')}\n \n \n \n \n Journal
\n \n \n ${topItem.getField(\'publicationTitle\')}\n \n \n \n \n 1st Author
\n \n \n ${topItem.getField(\'firstCreator\')}\n \n \n \n \n Authors
\n \n \n ${topItem.getCreators().map((v)=>v.firstName+" "+v.lastName).join("; ")}\n \n \n \n \n Pub. date
\n \n \n ${topItem.getField(\'date\')}\n \n \n \n \n DOI
\n \n \n ${topItem.getField(\'DOI\')} \n \n \n \n \n Archive
\n \n \n ${topItem.getField(\'archive\')}\n \n \n \n \n Archive Location
\n \n \n ${topItem.getField(\'archiveLocation\')}\n \n \n \n \n Call No.
\n \n \n ${topItem.getField(\'callNumber\')}\n \n \n
\n${itemNotes.map((noteItem)=>{\nconst noteLine = `\n\n ${noteItem.getNote()}\n Merge Date: ${new Date().toISOString().substr(0,10)+" "+ new Date().toTimeString()}
\n \n📝 Comments
\n\n Make your comments
\n
\n `;\ncopyNoteImage(noteItem);\nreturn noteLine;\n}).join("\\n")}\n',
- disabled: false,
- },
- {
- name: "[Item] collect annotations by color",
- text: '${await new Promise(async (r) => {\n async function getAnnotation(item) {\n try {\n if (!item || !item.isAnnotation()) {\n return null;\n }\n let json = await Zotero.Annotations.toJSON(item);\n json.id = item.key;\n delete json.key;\n for (let key in json) {\n json[key] = json[key] || "";\n }\n json.tags = json.tags || [];\n return json;\n } catch (e) {\n Zotero.logError(e);\n return null;\n }\n }\n\n async function getAnnotationsByColor(_item, colorFilter) {\n const annots = _item.getAnnotations().filter(colorFilter);\n if (annots.length === 0) {\n return {\n html: "",\n };\n }\n let annotations = [];\n for (let annot of annots) {\n const annotJson = await getAnnotation(annot);\n annotJson.attachmentItemID = _item.id;\n annotations.push(annotJson);\n }\n\n if (!editor) {\n alert("No active note editor detected. Please open workspace.");\n return r("");\n }\n await editor.importImages(annotations);\n return Zotero.EditorInstanceUtilities.serializeAnnotations(annotations);\n }\n\n const attachments = Zotero.Items.get(topItem.getAttachments()).filter((i) =>\n i.isPDFAttachment()\n );\n let res = "";\n const colors = ["#ffd400", "#ff6666", "#5fb236", "#2ea8e5", "#a28ae5"];\n const colorNames = ["Yellow", "Red", "Green", "Blue", "Purple"];\n for (let attachment of attachments) {\n res += `${attachment.getFilename()} `;\n for (let i in colors) {\n const renderedAnnotations = (\n await getAnnotationsByColor(\n attachment,\n (_annot) => _annot.annotationColor === colors[i]\n )\n ).html;\n if (renderedAnnotations) {\n res += `${colorNames[i]} Annotations
\n${renderedAnnotations}`;\n }\n }\n const renderedAnnotations = (\n await getAnnotationsByColor(\n attachment,\n (_annot) => !colors.includes(_annot.annotationColor)\n )\n ).html;\n if (renderedAnnotations) {\n res += `Other Annotations
\n${renderedAnnotations}`;\n }\n }\n r(res);\n})}',
- disabled: false,
- },
- {
- name: "[Item] collect annotations by tag",
- text: '// @beforeloop-begin\n${(()=>{\nsharedObj.tagRaw = prompt("Please input tags. Split with \'\',:", "");\nreturn "";\n})()}\n// @beforeloop-end\n// @default-begin\n${await new Promise(async (r) => {\n async function getAnnotation(item) {\n try {\n if (!item || !item.isAnnotation()) {\n return null;\n }\n let json = await Zotero.Annotations.toJSON(item);\n json.id = item.key;\n delete json.key;\n for (let key in json) {\n json[key] = json[key] || "";\n }\n json.tags = json.tags || [];\n return json;\n } catch (e) {\n Zotero.logError(e);\n return null;\n }\n }\n\n async function getAnnotationsByTag(_item, tag) {\n let annots = _item.getAnnotations();\n annots = tag.length? \n annots.filter((_annot) => _annot.getTags().map(_t=>_t.tag).includes(tag)) :\n annots;\n let annotations = [];\n for (let annot of annots) {\n const annotJson = await getAnnotation(annot);\n annotJson.attachmentItemID = _item.id;\n annotations.push(annotJson);\n }\n if (!editor) {\n alert("No active note editor detected. Please open workspace.");\n return r("");\n }\n await editor.importImages(annotations);\n return Zotero.EditorInstanceUtilities.serializeAnnotations(annotations);\n }\n const attachments = Zotero.Items.get(topItem.getAttachments()).filter((i) =>\n i.isPDFAttachment()\n );\n let res = "";\n if(!sharedObj.tagRaw){\n return;\n }\n res += `${topItem.getField("title")} `;\n for (let attachment of attachments) {\n res += `${attachment.getFilename()} `;\n for(tag of sharedObj.tagRaw.split(",").filter(t=>t.length)){\n res += `Tag: ${tag} `;\n const tags = (await getAnnotationsByTag(attachment, tag)).html\n res += tags ? tags : "No result
";\n }\n }\n r(res);\n})}\n// @default-end',
- disabled: false,
- },
- {
- name: "[Item] note links",
- text: '${topItem.getNoteTitle().trim() ? topItem.getNoteTitle().trim() : Zotero.BetterNotes.knowledge.getNoteLink(topItem)}
',
- disabled: false,
- },
- {
- name: "[Text] table",
- text: '${(() => {\nconst size = prompt("Table Size(row*column):", "4*3");\nif (!size) {\nreturn "";\n}\nconst row = Number(size.split("*")[0]);\nconst col = Number(size.split("*")[1]);\nif (!row || !col) {\nreturn "";\n}\nconst makeHeadCell = () => "\n ";\nconst makeHead = () =>\n`${[...Array(col).keys()].map(makeHeadCell).join("\n")} `;\nconst makeCell = () => "\n ";\nconst makeRow = () =>\n`${[...Array(col).keys()].map(makeCell).join("\n")} `;\nreturn `${makeHead()} \n${\nrow > 1\n? "" +\n[...Array(row - 1).keys()].map(makeRow).join("\n") +\n" "\n: ""\n}\n
`;\n})()}',
- disabled: false,
- },
- ];
- }
-
- async renderTemplateAsync(
- key: string,
- argString: string = "",
- argList: any[] = [],
- useDefault: boolean = true,
- stage: string = "default"
- ) {
- this._Addon.toolkit.Tool.log(`renderTemplateAsync: ${key}`);
- let templateText = this.getTemplateText(key);
- if (useDefault && !templateText) {
- templateText = this._defaultTemplates.find((t) => t.name === key).text;
- if (!templateText) {
- return "";
- }
- }
-
- const templateLines = templateText.split("\n");
- let startIndex = templateLines.indexOf(`// @${stage}-begin`),
- endIndex = templateLines.indexOf(`// @${stage}-end`);
- if (startIndex < 0 && endIndex < 0 && stage !== "default") {
- // Skip this stage
- return "";
- }
- if (startIndex < 0) {
- // We skip the flag line later
- startIndex = -1;
- }
- if (endIndex < 0) {
- endIndex = templateLines.length;
- }
- // Skip the flag lines
- templateText = templateLines.slice(startIndex + 1, endIndex).join("\n");
-
- let _newLine: string = "";
- try {
- const AsyncFunction = Object.getPrototypeOf(
- async function () {}
- ).constructor;
- const _ = new AsyncFunction(argString, "return `" + templateText + "`");
- this._Addon.toolkit.Tool.log(_);
- _newLine = await _(...argList);
- } catch (e) {
- alert(`Template ${key} Error: ${e}`);
- this._Addon.toolkit.Tool.log(e);
- return "";
- }
- return _newLine;
- }
-
- getTemplateKeys(): NoteTemplate[] {
- let templateKeys = Zotero.Prefs.get(
- "Knowledge4Zotero.templateKeys"
- ) as string;
- return templateKeys ? JSON.parse(templateKeys) : [];
- }
-
- getTemplateKey(keyName: string): NoteTemplate {
- return this.getTemplateKeys().filter((t) => t.name === keyName)[0];
- }
-
- setTemplateKeys(templateKeys: NoteTemplate[]): void {
- Zotero.Prefs.set(
- "Knowledge4Zotero.templateKeys",
- JSON.stringify(templateKeys)
- );
- }
-
- addTemplateKey(key: NoteTemplate): boolean {
- const templateKeys = this.getTemplateKeys();
- if (templateKeys.map((t) => t.name).includes(key.name)) {
- return false;
- }
- templateKeys.push(key);
- this.setTemplateKeys(templateKeys);
- return true;
- }
-
- removeTemplateKey(keyName: string): boolean {
- const templateKeys = this.getTemplateKeys();
- if (!templateKeys.map((t) => t.name).includes(keyName)) {
- return false;
- }
- templateKeys.splice(templateKeys.map((t) => t.name).indexOf(keyName), 1);
- this.setTemplateKeys(templateKeys);
- return true;
- }
-
- getTemplateText(keyName: string): string {
- let template = Zotero.Prefs.get(
- `Knowledge4Zotero.template.${keyName}`
- ) as string;
- if (!template) {
- template = "";
- Zotero.Prefs.set(`Knowledge4Zotero.template.${keyName}`, template);
- }
- return template;
- }
-
- setTemplate(key: NoteTemplate, template: string = ""): void {
- let _key = JSON.parse(JSON.stringify(key));
- if (_key.text) {
- template = _key.text;
- delete _key.text;
- }
- this.addTemplateKey(_key);
- Zotero.Prefs.set(`Knowledge4Zotero.template.${_key.name}`, template);
- }
-
- removeTemplate(keyName: string): void {
- this.removeTemplateKey(keyName);
- Zotero.Prefs.clear(`Knowledge4Zotero.template.${keyName}`);
- }
-
- resetTemplates() {
- let oldTemplatesRaw: string = Zotero.Prefs.get(
- "Knowledge4Zotero.noteTemplate"
- ) as string;
- // Convert old version
- if (oldTemplatesRaw) {
- const templates: NoteTemplate[] = JSON.parse(oldTemplatesRaw);
- for (const template of templates) {
- this._Addon.TemplateController.setTemplate(template);
- }
- Zotero.Prefs.clear("Knowledge4Zotero.noteTemplate");
- }
- // Convert buggy template
- if (
- !this._Addon.TemplateController.getTemplateText(
- "[QuickBackLink]"
- ).includes("ignore=1")
- ) {
- this._Addon.TemplateController.setTemplate(
- this._Addon.TemplateController._defaultTemplates.find(
- (t) => t.name === "[QuickBackLink]"
- )
- );
- this._Addon.ZoteroViews.showProgressWindow(
- "Better Notes",
- "The [QuickBackLink] is reset because of missing ignore=1 in link."
- );
- }
- let templateKeys = this._Addon.TemplateController.getTemplateKeys();
- const currentNames = templateKeys.map((t) => t.name);
- for (const defaultTemplate of this._Addon.TemplateController
- ._defaultTemplates) {
- if (!currentNames.includes(defaultTemplate.name)) {
- this._Addon.TemplateController.setTemplate(defaultTemplate);
- }
- }
- }
-
- getCitationStyle(): {
- mode: string;
- contentType: string;
- id: string;
- locale: string;
- } {
- let format = Zotero.Prefs.get("Knowledge4Zotero.citeFormat") as string;
- try {
- if (format) {
- format = JSON.parse(format);
- } else {
- throw Error("format not initialized");
- }
- } catch (e) {
- format = Zotero.QuickCopy.getFormatFromURL(
- Zotero.QuickCopy.lastActiveURL
- );
- format = Zotero.QuickCopy.unserializeSetting(format);
- Zotero.Prefs.set("Knowledge4Zotero.citeFormat", JSON.stringify(format));
- }
- return format as any;
- }
-}
-
-/*
- * This part is for the template usage only
- * to keep API consistency
- */
-class TemplateAPI extends AddonBase {
- constructor(parent: BetterNotes) {
- super(parent);
- }
-
- public getNoteLink(
- note: Zotero.Item,
- options: {
- ignore?: boolean;
- withLine?: boolean;
- } = { ignore: false, withLine: false }
- ) {
- return this._Addon.NoteUtils.getNoteLink(note, options);
- }
-
- public async getWorkspaceEditorInstance(
- type: "main" | "preview" = "main",
- wait: boolean = true
- ) {
- return await this._Addon.WorkspaceWindow.getWorkspaceEditorInstance(
- type,
- wait
- );
- }
-}
-
-export { TemplateController, TemplateAPI };
diff --git a/src/template/templateWindow.ts b/src/template/templateWindow.ts
deleted file mode 100644
index e88c7a0..0000000
--- a/src/template/templateWindow.ts
+++ /dev/null
@@ -1,232 +0,0 @@
-/*
- * This file contains template window related code.
- */
-
-import BetterNotes from "../addon";
-import { NoteTemplate } from "../utils";
-import AddonBase from "../module";
-
-class TemplateWindow extends AddonBase {
- private _window: Window;
- constructor(parent: BetterNotes) {
- super(parent);
- }
-
- openEditor() {
- if (this._window && !this._window.closed) {
- this._window.focus();
- } else {
- window.open(
- "chrome://Knowledge4Zotero/content/template.xul",
- "_blank",
- "chrome,extrachrome,centerscreen,width=800,height=400,resizable=yes"
- );
- }
- }
-
- initTemplates(_window: Window) {
- this._window = _window;
- this.updateTemplateView();
- }
-
- getSelectedTemplateName(): string {
- const listbox = this._window.document.getElementById(
- "template-list"
- ) as XUL.ListItem;
- const selectedItem = listbox.selectedItem;
- if (selectedItem) {
- const name = selectedItem.getAttribute("id");
- return name;
- }
- return "";
- }
-
- updateTemplateView() {
- const templates = this._Addon.TemplateController.getTemplateKeys();
- const listbox = this._window.document.getElementById("template-list");
- let e,
- es = this._window.document.getElementsByTagName("listitem");
- while (es.length > 0) {
- e = es[0];
- e.parentElement.removeChild(e);
- }
- for (const template of templates) {
- const listitem = this._Addon.toolkit.UI.createElement(
- this._window.document,
- "listitem",
- "xul"
- ) as XUL.ListItem;
- listitem.setAttribute("id", template.name);
- const name = this._Addon.toolkit.UI.createElement(
- this._window.document,
- "listcell",
- "xul"
- ) as XUL.Element;
- name.setAttribute("label", template.name);
- if (
- this._Addon.TemplateController._systemTemplateNames.includes(
- template.name
- )
- ) {
- listitem.style.color = "#f2ac46";
- }
- listitem.append(name);
- listbox.append(listitem);
- }
- this.updateEditorView();
- }
-
- updateEditorView() {
- this._Addon.toolkit.Tool.log("update editor");
- this._Addon.toolkit.Tool.log("update editor");
- const name = this.getSelectedTemplateName();
- const templateText = this._Addon.TemplateController.getTemplateText(name);
-
- const header = this._window.document.getElementById(
- "editor-name"
- ) as XUL.Textbox;
- const text = this._window.document.getElementById(
- "editor-textbox"
- ) as XUL.Textbox;
- const saveTemplate = this._window.document.getElementById("save-template");
- const deleteTemplate =
- this._window.document.getElementById("delete-template");
- const resetTemplate =
- this._window.document.getElementById("reset-template");
- if (!name) {
- header.value = "";
- header.setAttribute("disabled", "true");
- text.value = "";
- text.setAttribute("disabled", "true");
- saveTemplate.setAttribute("disabled", "true");
- deleteTemplate.setAttribute("disabled", "true");
- deleteTemplate.hidden = false;
- resetTemplate.hidden = true;
- } else {
- header.value = name;
- if (!this._Addon.TemplateController._systemTemplateNames.includes(name)) {
- header.removeAttribute("disabled");
- deleteTemplate.hidden = false;
- resetTemplate.hidden = true;
- } else {
- header.setAttribute("disabled", "true");
- deleteTemplate.setAttribute("disabled", "true");
- deleteTemplate.hidden = true;
- resetTemplate.hidden = false;
- }
- text.value = templateText;
- text.removeAttribute("disabled");
- saveTemplate.removeAttribute("disabled");
- deleteTemplate.removeAttribute("disabled");
- }
- }
-
- createTemplate() {
- const template: NoteTemplate = {
- name: `New Template: ${new Date().getTime()}`,
- text: "",
- disabled: false,
- };
- this._Addon.TemplateController.setTemplate(template);
- this.updateTemplateView();
- }
-
- async importNoteTemplate() {
- const io = {
- // Not working
- singleSelection: true,
- dataIn: null,
- dataOut: null,
- deferred: Zotero.Promise.defer(),
- };
-
- (window as unknown as XUL.XULWindow).openDialog(
- "chrome://zotero/content/selectItemsDialog.xul",
- "",
- "chrome,dialog=no,centerscreen,resizable=yes",
- io
- );
- await io.deferred.promise;
-
- const ids = io.dataOut as number[];
- const note: Zotero.Item = Zotero.Items.get(ids).filter(
- (item: Zotero.Item) => item.isNote()
- )[0];
- if (!note) {
- return;
- }
- const template: NoteTemplate = {
- name: `Template from ${note.getNoteTitle()}: ${new Date().getTime()}`,
- text: note.getNote(),
- disabled: false,
- };
- this._Addon.TemplateController.setTemplate(template);
- this.updateTemplateView();
- }
-
- saveSelectedTemplate() {
- const name = this.getSelectedTemplateName();
- const header = this._window.document.getElementById(
- "editor-name"
- ) as XUL.Textbox;
- const text = this._window.document.getElementById(
- "editor-textbox"
- ) as XUL.Textbox;
-
- if (
- this._Addon.TemplateController._systemTemplateNames.includes(name) &&
- header.value !== name
- ) {
- this._Addon.ZoteroViews.showProgressWindow(
- "Better Notes",
- `Template ${name} is a system template. Modifying template name is not allowed.`
- );
- return;
- }
-
- const template = this._Addon.TemplateController.getTemplateKey(name);
- template.name = header.value;
- template.text = text.value;
- this._Addon.TemplateController.setTemplate(template);
- if (name !== template.name) {
- this._Addon.TemplateController.removeTemplate(name);
- }
- this._Addon.ZoteroViews.showProgressWindow(
- "Better Notes",
- `Template ${template.name} saved.`
- );
-
- this.updateTemplateView();
- }
-
- deleteSelectedTemplate() {
- const name = this.getSelectedTemplateName();
- if (this._Addon.TemplateController._systemTemplateNames.includes(name)) {
- this._Addon.ZoteroViews.showProgressWindow(
- "Better Notes",
- `Template ${name} is a system template. Removing system template is note allowed.`
- );
- return;
- }
- this._Addon.TemplateController.removeTemplate(name);
- this.updateTemplateView();
- }
-
- resetSelectedTemplate() {
- const name = this.getSelectedTemplateName();
- if (this._Addon.TemplateController._systemTemplateNames.includes(name)) {
- const text = this._window.document.getElementById(
- "editor-textbox"
- ) as XUL.Textbox;
- text.value = this._Addon.TemplateController._defaultTemplates.find(
- (t) => t.name === name
- ).text;
- this._Addon.ZoteroViews.showProgressWindow(
- "Better Notes",
- `Template ${name} is reset. Please save before leaving.`
- );
- }
- }
-}
-
-export default TemplateWindow;
diff --git a/src/utils.ts b/src/utils.ts
deleted file mode 100644
index 6a23b8c..0000000
--- a/src/utils.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * This file contains definitions of commonly used structures.
- */
-
-class EditorMessage {
- public type: string;
- public content: {
- event?: XUL.XULEvent;
- editorInstance?: Zotero.EditorInstance;
- params?: any;
- };
- constructor(type: string, content: object) {
- this.type = type;
- this.content = content;
- }
-}
-
-enum OutlineType {
- treeView = 1,
- mindMap,
- bubbleMap,
-}
-
-class NoteTemplate {
- name: string;
- disabled: boolean;
- text?: string;
-}
-
-enum SyncCode {
- UpToDate = 0,
- NoteAhead,
- MDAhead,
- NeedDiff,
-}
-
-enum NodeMode {
- default = 0,
- wrap,
- replace,
- direct,
-}
-
-export { EditorMessage, OutlineType, NoteTemplate, SyncCode, NodeMode };
diff --git a/src/utils/annotation.ts b/src/utils/annotation.ts
new file mode 100644
index 0000000..f34c105
--- /dev/null
+++ b/src/utils/annotation.ts
@@ -0,0 +1,244 @@
+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;
+ } catch (e: unknown) {
+ Zotero.logError(e as Error);
+ return null;
+ }
+}
+
+// Zotero.EditorInstanceUtilities.serializeAnnotations
+function serializeAnnotations(
+ annotations: Required[],
+ 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.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 = `${formatted} `;
+ }
+
+ // Image
+ if (annotation.imageAttachmentKey) {
+ // // let imageAttachmentKey = await this._importImage(annotation.image);
+ // delete annotation.image;
+
+ // 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 = ` `;
+ }
+
+ // Text
+ if (annotation.text) {
+ let text = Zotero.EditorInstanceUtilities._transformTextToHTML.call(
+ Zotero.EditorInstanceUtilities,
+ annotation.text.trim()
+ );
+ highlightHTML = `${text} `;
+ quotedHighlightHTML = `${Zotero.getString(
+ "punctuation.openingQMark"
+ )}${text}${Zotero.getString("punctuation.closingQMark")} `;
+ }
+
+ // 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 = "{{image}} {{citation}} {{comment}}
";
+ }
+
+ ztoolkit.log("Using note template:");
+ ztoolkit.log(template);
+
+ template = template.replace(
+ /([^<>]*?)({{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,
+ annotations: CustomAnnotationJSON[]
+) {
+ for (let annotation of annotations) {
+ if (annotation.image) {
+ 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!);
+ }
+ options.noteItem &&
+ (await importAnnotationImagesToNote(options.noteItem, annotationJSONList));
+ const html = serializeAnnotations(
+ annotationJSONList as Required[],
+ false,
+ options.skipCitation
+ ).html;
+ return html;
+}
diff --git a/src/utils/citation.ts b/src/utils/citation.ts
new file mode 100644
index 0000000..b263f74
--- /dev/null
+++ b/src/utils/citation.ts
@@ -0,0 +1,34 @@
+export async function parseCitationHTML(citationIds: number[]) {
+ let html = "";
+ let items = await Zotero.Items.getAsync(citationIds);
+ for (let item of items) {
+ if (
+ item.isNote() &&
+ !(await Zotero.Notes.ensureEmbeddedImagesAreAvailable(item)) &&
+ !Zotero.Notes.promptToIgnoreMissingImage()
+ ) {
+ return null;
+ }
+ }
+
+ for (let item of items) {
+ if (item.isRegularItem()) {
+ // @ts-ignore
+ let itemData = Zotero.Utilities.Item.itemToCSLJSON(item);
+ let citation = {
+ citationItems: [
+ {
+ uris: [Zotero.URI.getItemURI(item)],
+ itemData,
+ },
+ ],
+ properties: {},
+ };
+ let formatted = Zotero.EditorInstanceUtilities.formatCitation(citation);
+ html += `${formatted}
`;
+ }
+ }
+ return html;
+}
diff --git a/src/utils/config.ts b/src/utils/config.ts
new file mode 100644
index 0000000..e1593ea
--- /dev/null
+++ b/src/utils/config.ts
@@ -0,0 +1,165 @@
+export const ICONS = {
+ settings: ` `,
+ addon: ` `,
+ linkAfter: `
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ...
+
+ `,
+ linkBefore: `
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ...
+
+ `,
+ previewImage: ` `,
+ resizeImage: ` `,
+ imageViewerPin: ` `,
+ imageViewerPined: ` `,
+ switchOutline: `
+
+ `,
+ saveOutlineImage: `
+
+
+
+
+ `,
+ saveOutlineFreeMind: `
+
+ `,
+ workspace_outline_collapsed: ` `,
+ workspace_outline_open: ` `,
+ workspace_preview_collapsed: ` `,
+ workspace_preview_open: ` `,
+ workspace_notes_collapsed: ` w`,
+ workspace_notes_open: ` `,
+ readerQuickNote: ` `,
+ readerOCR: ` `,
+ // old
+ addCitation: ` `,
+ notMainKnowledge: ` `,
+ isMainKnowledge: ` `,
+ openAttachment: ` `,
+ switchEditor: ` `,
+ export: ` `,
+ close: ` `,
+ embedLinkContent: ` `,
+ updateLinkText: ` `,
+ openInNewWindow: ` `,
+};
+
+export const PROGRESS_TITLE = "Better Notes";
diff --git a/src/utils/editor.ts b/src/utils/editor.ts
new file mode 100644
index 0000000..9d2a163
--- /dev/null
+++ b/src/utils/editor.ts
@@ -0,0 +1,285 @@
+import TreeModel = require("tree-model");
+import { TextSelection } from "prosemirror-state";
+
+export {
+ insert,
+ del,
+ scroll,
+ getEditorInstance,
+ moveHeading,
+ updateHeadingTextAtLine,
+ getEditorCore,
+ getLineAtCursor,
+ getPositionAtLine,
+ getURLAtCursor,
+ updateImageDimensionsAtCursor,
+ updateURLAtCursor,
+ getContentInLines,
+};
+
+function insert(
+ editor: Zotero.EditorInstance,
+ content: string = "",
+ position: number | "end" | "start" | "cursor" = "cursor"
+) {
+ const core = getEditorCore(editor);
+ if (position === "cursor") {
+ position = getPositionAtCursor(editor);
+ } else if (position === "end") {
+ position = core.view.state.doc.content.size;
+ } else if (position === "start") {
+ position = 0;
+ }
+ position = Math.max(0, Math.min(position, core.view.state.doc.content.size));
+ (core as any).insertHTML(position, content);
+}
+
+function del(editor: Zotero.EditorInstance, from: number, to: number) {
+ const core = getEditorCore(editor);
+ const EditorAPI = getEditorAPI(editor);
+ EditorAPI.deleteRange(from, to)(core.view.state, core.view.dispatch);
+}
+
+function scroll(editor: Zotero.EditorInstance, lineIndex: number) {
+ const core = getEditorCore(editor);
+ const dom = getDOMAtLine(editor, lineIndex);
+ const offset = dom.offsetTop;
+ core.view.dom.parentElement?.scrollTo(0, offset);
+}
+
+function getEditorInstance(noteId: number) {
+ const editor = Zotero.Notes._editorInstances.find(
+ (e) =>
+ e._item.id === noteId && !Components.utils.isDeadWrapper(e._iframeWindow)
+ );
+ return editor;
+}
+
+function getEditorCore(editor: Zotero.EditorInstance): EditorCore {
+ return (editor._iframeWindow as any).wrappedJSObject._currentEditorInstance
+ ._editorCore;
+}
+
+function getEditorAPI(editor: Zotero.EditorInstance) {
+ return (editor._iframeWindow as any).wrappedJSObject
+ .BetterNotesEditorAPI as EditorAPI;
+}
+
+function getPositionAtCursor(editor: Zotero.EditorInstance) {
+ const selection = getEditorCore(editor).view.state.selection;
+ return selection.$anchor.after(selection.$anchor.depth);
+}
+
+function getLineAtCursor(editor: Zotero.EditorInstance) {
+ const position = getPositionAtCursor(editor);
+ let i = 0;
+ let lastPos = 0;
+ let currentPos = getPositionAtLine(editor, 0);
+ while (currentPos !== lastPos) {
+ if (position <= currentPos) {
+ break;
+ }
+ i += 1;
+ lastPos = currentPos;
+ currentPos = getPositionAtLine(editor, i);
+ }
+ return i;
+}
+
+function getDOMAtLine(
+ editor: Zotero.EditorInstance,
+ lineIndex: number
+): HTMLElement {
+ const core = getEditorCore(editor);
+ const lineNodeDesc = core.view.docView.children[lineIndex];
+ return lineNodeDesc?.dom;
+}
+
+function getPositionAtLine(
+ editor: Zotero.EditorInstance,
+ lineIndex: number,
+ type: "start" | "end" = "end"
+): number {
+ const core = getEditorCore(editor);
+ const lineNodeDesc = core.view.docView.children[lineIndex];
+ const linePos = lineNodeDesc ? core.view.posAtDOM(lineNodeDesc.dom, 0) : 0;
+ return Math.max(
+ 0,
+ Math.min(
+ type === "end" ? linePos + lineNodeDesc.size - 1 : linePos - 1,
+ core.view.state.tr.doc.content.size
+ )
+ );
+}
+
+function getURLAtCursor(editor: Zotero.EditorInstance) {
+ const core = getEditorCore(editor);
+ return core.pluginState.link.getHref(core.view.state);
+}
+
+function updateURLAtCursor(
+ editor: Zotero.EditorInstance,
+ text: string | undefined,
+ url: string
+) {
+ const core = getEditorCore(editor);
+ const EditorAPI = getEditorAPI(editor);
+ let from = core.view.state.selection.from;
+ let to = core.view.state.selection.to;
+ const schema = core.view.state.schema;
+ if (!url) {
+ return;
+ }
+
+ EditorAPI.replaceRangeAtCursor(
+ text,
+ schema.marks.link,
+ JSON.stringify({ href: url }),
+ schema.marks.link
+ )(core.view.state, core.view.dispatch);
+ EditorAPI.refocusEditor(() => {
+ core.view.dispatch(
+ core.view.state.tr.setSelection(
+ TextSelection.create(core.view.state.tr.doc, from, to)
+ )
+ );
+ });
+}
+
+function updateHeadingTextAtLine(
+ editor: Zotero.EditorInstance,
+ lineIndex: number,
+ text: string
+) {
+ const core = getEditorCore(editor);
+ const schema = core.view.state.schema;
+ const EditorAPI = getEditorAPI(editor);
+
+ const from = getPositionAtLine(editor, lineIndex, "start");
+ const to = getPositionAtLine(editor, lineIndex, "end");
+ const level = EditorAPI.getHeadingLevelInRange(from, to)(core.view.state);
+ EditorAPI.replaceRangeNode(
+ from,
+ to,
+ text,
+ schema.nodes.heading,
+ JSON.stringify({ level })
+ )(core.view.state, core.view.dispatch);
+ EditorAPI.refocusEditor(() => {
+ core.view.dispatch(
+ core.view.state.tr.setSelection(
+ TextSelection.create(core.view.state.tr.doc, from, from + text.length)
+ )
+ );
+ });
+}
+
+function isImageAtCursor(editor: Zotero.EditorInstance) {
+ return (
+ // @ts-ignore
+ getEditorCore(editor).view.state.selection.node?.type?.name === "image"
+ );
+}
+
+function updateImageDimensionsAtCursor(
+ editor: Zotero.EditorInstance,
+ width: number
+) {
+ const core = getEditorCore(editor);
+ const EditorAPI = getEditorAPI(editor);
+ EditorAPI.updateImageDimensions(
+ // @ts-ignore
+ core.view.state.selection.node.attrs.nodeID,
+ width,
+ undefined,
+ core.view.state,
+ core.view.dispatch
+ );
+}
+
+function moveLines(
+ editor: Zotero.EditorInstance,
+ fromIndex: number,
+ toIndex: number,
+ targetIndex: number
+) {
+ const core = getEditorCore(editor);
+ const EditorAPI = getEditorAPI(editor);
+ const from = getPositionAtLine(editor, fromIndex, "start");
+ const to = getPositionAtLine(editor, toIndex, "end");
+ const target = getPositionAtLine(editor, targetIndex, "start");
+ let delta = 0;
+ if (target < from) {
+ delta = target - from;
+ } else if (target > to) {
+ delta = target - to;
+ } else {
+ throw new Error("Invalid move");
+ }
+ EditorAPI.moveRange(from, to, delta)(core.view.state, core.view.dispatch);
+ EditorAPI.refocusEditor(() => {
+ core.view.dispatch(
+ core.view.state.tr.setSelection(
+ TextSelection.create(core.view.state.tr.doc, target, target + to - from)
+ )
+ );
+ });
+}
+
+function moveHeading(
+ editor: Zotero.EditorInstance | undefined,
+ currentNode: TreeModel.Node,
+ targetNode: TreeModel.Node,
+ as: "child" | "before" | "after"
+) {
+ if (!editor || targetNode.getPath().indexOf(currentNode) >= 0) {
+ return;
+ }
+
+ let targetIndex = 0;
+ let targetLevel = 1;
+
+ if (as === "child") {
+ targetIndex = targetNode.model.endIndex + 1;
+ targetLevel = targetNode.model.level === 6 ? 6 : targetNode.model.level + 1;
+ } else if (as === "before") {
+ targetIndex = targetNode.model.lineIndex;
+ targetLevel =
+ targetNode.model.level === 7
+ ? targetNode.parent.model.level === 6
+ ? 6
+ : targetNode.parent.model.level + 1
+ : targetNode.model.level;
+ } else if (as === "after") {
+ targetIndex = targetNode.model.endIndex + 1;
+ targetLevel =
+ targetNode.model.level === 7
+ ? targetNode.parent.model.level === 6
+ ? 6
+ : targetNode.parent.model.level + 1
+ : targetNode.model.level;
+ }
+
+ const fromIndex = currentNode.model.lineIndex;
+ const toIndex = currentNode.model.endIndex;
+ let levelChange = targetLevel - currentNode.model.level;
+ const core = getEditorCore(editor);
+ const EditorAPI = getEditorAPI(editor);
+ EditorAPI.updateHeadingsInRange(
+ getPositionAtLine(editor, fromIndex, "start"),
+ getPositionAtLine(editor, toIndex, "end"),
+ levelChange
+ )(core.view.state, core.view.dispatch);
+ moveLines(editor, fromIndex, toIndex, targetIndex);
+}
+
+function getContentInLines(
+ editor: Zotero.EditorInstance,
+ fromIndex: number,
+ toIndex: number
+) {
+ const core = getEditorCore(editor);
+ const from = getPositionAtLine(editor, fromIndex, "start");
+ const to = getPositionAtLine(editor, toIndex, "end");
+ return core.view.state.doc.textBetween(from, to);
+}
diff --git a/src/utils/hint.ts b/src/utils/hint.ts
new file mode 100644
index 0000000..0c3afb9
--- /dev/null
+++ b/src/utils/hint.ts
@@ -0,0 +1,36 @@
+import { PROGRESS_TITLE } from "./config";
+import { waitUtilAsync } from "./wait";
+
+function showHint(text: string) {
+ return new ztoolkit.ProgressWindow(PROGRESS_TITLE)
+ .createLine({ text, progress: 100, type: "default" })
+ .show();
+}
+
+async function showHintWithLink(
+ text: string,
+ linkText: string,
+ linkCallback: (ev: MouseEvent) => any
+) {
+ const progress = new ztoolkit.ProgressWindow(PROGRESS_TITLE)
+ .createLine({ text, progress: 100, type: "default" })
+ .show(-1);
+ // Just a placeholder
+ progress.addDescription(`${linkText} `);
+
+ await waitUtilAsync(() =>
+ // @ts-ignore
+ Boolean(progress.lines && progress.lines[0]._itemText)
+ );
+ // @ts-ignore
+ progress.lines[0]._hbox.ownerDocument
+ .querySelector("label[href]")
+ .addEventListener("click", async (ev: MouseEvent) => {
+ ev.stopPropagation();
+ ev.preventDefault();
+ linkCallback(ev);
+ });
+ return progress;
+}
+
+export { showHint, showHintWithLink };
diff --git a/src/utils/itemPicker.ts b/src/utils/itemPicker.ts
new file mode 100644
index 0000000..1638b70
--- /dev/null
+++ b/src/utils/itemPicker.ts
@@ -0,0 +1,21 @@
+export async function itemPicker() {
+ const io = {
+ singleSelection: false,
+ dataIn: null as unknown,
+ dataOut: null as unknown,
+ deferred: Zotero.Promise.defer(),
+ };
+
+ (window as unknown as XUL.XULWindow).openDialog(
+ ztoolkit.isZotero7()
+ ? "chrome://zotero/content/selectItemsDialog.xhtml"
+ : "chrome://zotero/content/selectItemsDialog.xul",
+ "",
+ "chrome,dialog=no,centerscreen,resizable=yes",
+ io
+ );
+ await io.deferred.promise;
+
+ const itemIds = io.dataOut as number[];
+ return itemIds;
+}
diff --git a/src/utils/link.ts b/src/utils/link.ts
new file mode 100644
index 0000000..c103fb0
--- /dev/null
+++ b/src/utils/link.ts
@@ -0,0 +1,103 @@
+export function getNoteLinkParams(link: string) {
+ try {
+ const url = new (ztoolkit.getGlobal("URL"))(link);
+ const pathList = url.pathname.split("/").filter((s) => s);
+ const noteKey = pathList.pop();
+ const id = pathList.pop();
+ let libraryID: number;
+ if (id === "u") {
+ libraryID = Zotero.Libraries.userLibraryID;
+ } else {
+ libraryID = Zotero.Groups.getLibraryIDFromGroupID(id);
+ }
+ const line = url.searchParams.get("line");
+ return {
+ link,
+ libraryID,
+ noteKey,
+ noteItem: Zotero.Items.getByLibraryAndKey(libraryID, noteKey || "") as
+ | Zotero.Item
+ | false,
+ ignore: Boolean(url.searchParams.get("ignore")),
+ lineIndex: typeof line === "string" ? parseInt(line) : null,
+ };
+ } catch (e: unknown) {
+ return {
+ link,
+ libraryID: -1,
+ noteKey: undefined,
+ noteItem: false as false,
+ ignore: null,
+ lineIndex: null,
+ };
+ }
+}
+
+export function getNoteLink(
+ noteItem: Zotero.Item,
+ options: {
+ ignore?: boolean | null;
+ lineIndex?: number | null;
+ } = {}
+) {
+ const libraryID = noteItem.libraryID;
+ const library = Zotero.Libraries.get(libraryID);
+ if (!library) {
+ return;
+ }
+ let groupID: string;
+ if (library.libraryType === "user") {
+ groupID = "u";
+ } else if (library.libraryType === "group") {
+ groupID = `${library.id}`;
+ } else {
+ return "";
+ }
+ const noteKey = noteItem.key;
+ let link = `zotero://note/${groupID}/${noteKey}/`;
+ const addParam = (link: string, param: string): string => {
+ const lastChar = link[link.length - 1];
+ if (lastChar === "/") {
+ link += "?";
+ } else if (lastChar !== "?" && lastChar !== "&") {
+ link += "&";
+ }
+ return `${link}${param}`;
+ };
+ if (Object.keys(options).length) {
+ if (options.ignore) {
+ link = addParam(link, "ignore=1");
+ }
+ if (options.lineIndex) {
+ link = addParam(link, `line=${options.lineIndex}`);
+ }
+ }
+ return link;
+}
+
+export function getLinkedNotesRecursively(
+ link: string,
+ ignoreIds: number[] = []
+) {
+ const linkParams = getNoteLinkParams(link);
+ if (!linkParams.noteItem) return [];
+ const noteItem = linkParams.noteItem;
+ if (ignoreIds.includes(noteItem.id)) {
+ return [];
+ }
+ const doc = ztoolkit
+ .getDOMParser()
+ .parseFromString(noteItem.getNote(), "text/html");
+ const links = Array.from(doc.querySelectorAll("a"));
+ return links.reduce(
+ (acc, link) => {
+ const linkParams = getNoteLinkParams(link.href);
+ if (linkParams.noteItem) {
+ acc.push(linkParams.noteItem.id);
+ acc.push(...getLinkedNotesRecursively(link.href, acc));
+ }
+ return acc;
+ },
+ [linkParams.noteItem.id] as number[]
+ );
+}
diff --git a/src/utils/locale.ts b/src/utils/locale.ts
new file mode 100644
index 0000000..6027f2a
--- /dev/null
+++ b/src/utils/locale.ts
@@ -0,0 +1,23 @@
+import { config } from "../../package.json";
+
+export function initLocale() {
+ addon.data.locale = {
+ stringBundle: Components.classes["@mozilla.org/intl/stringbundle;1"]
+ .getService(Components.interfaces.nsIStringBundleService)
+ .createBundle(`chrome://${config.addonRef}/locale/addon.properties`),
+ };
+}
+
+export function getString(localeString: string): string {
+ switch (localeString) {
+ case "alt":
+ return Zotero.isMac ? "⌥" : "Alt";
+ case "ctrl":
+ return Zotero.isMac ? "⌘" : "Ctrl";
+ }
+ try {
+ return addon.data.locale?.stringBundle.GetStringFromName(localeString);
+ } catch (e) {
+ return localeString;
+ }
+}
diff --git a/src/utils/note.ts b/src/utils/note.ts
new file mode 100644
index 0000000..4b0e521
--- /dev/null
+++ b/src/utils/note.ts
@@ -0,0 +1,485 @@
+import TreeModel = require("tree-model");
+import { getEditorInstance, getPositionAtLine, insert } from "./editor";
+import { getItemDataURL } from "./str";
+
+export {
+ renderNoteHTML,
+ parseHTMLLines,
+ getLinesInNote,
+ addLineToNote,
+ getNoteType,
+ getNoteTree,
+ getNoteTreeFlattened,
+ getNoteTreeNodeById,
+ copyEmbeddedImagesFromNote,
+ copyEmbeddedImagesInHTML,
+ importImageToNote,
+};
+
+function parseHTMLLines(html: string): string[] {
+ let containerIndex = html.search(/data-schema-version="[0-9]*">/g);
+ if (containerIndex != -1) {
+ html = html.substring(
+ containerIndex + 'data-schema-version="8">'.length,
+ html.length - "
".length
+ );
+ }
+ let noteLines = html.split("\n").filter((e) => e);
+
+ // A cache for temporarily stored lines
+ let previousLineCache = [];
+ let nextLineCache = [];
+
+ const forceInline = ["table", "blockquote", "pre", "ol", "ul"];
+ const selfInline: string[] = [];
+ let forceInlineStack = [];
+ let forceInlineFlag = false;
+ let selfInlineFlag = false;
+
+ const parsedLines = [];
+ for (let line of noteLines) {
+ // restore self inline flag
+ selfInlineFlag = false;
+
+ // For force inline tags, set flag to append lines to current line
+ for (const tag of forceInline) {
+ const startReg = `<${tag}`;
+ const isStart = line.includes(startReg);
+ const endReg = `${tag}>`;
+ const isEnd = line.includes(endReg);
+ if (isStart && !isEnd) {
+ forceInlineStack.push(tag);
+ ztoolkit.log("push", tag, line, forceInlineStack);
+ forceInlineFlag = true;
+ break;
+ }
+ if (isEnd && !isStart) {
+ forceInlineStack.pop();
+ ztoolkit.log("pop", tag, line, forceInlineStack);
+ // Exit force inline mode if the stack is empty
+ if (forceInlineStack.length === 0) {
+ forceInlineFlag = false;
+ }
+ break;
+ }
+ }
+
+ if (forceInlineFlag) {
+ nextLineCache.push(line);
+ } else {
+ // For self inline tags, cache start as previous line and end as next line
+ for (const tag of selfInline) {
+ const isStart = line.includes(`<${tag}`);
+ const isEnd = line.includes(`${tag}>`);
+ if (isStart && !isEnd) {
+ selfInlineFlag = true;
+ nextLineCache.push(line);
+ break;
+ }
+ if (!isStart && isEnd) {
+ selfInlineFlag = true;
+ previousLineCache.push(line);
+ break;
+ }
+ }
+
+ if (!selfInlineFlag) {
+ // Append cache to previous line
+ if (previousLineCache.length) {
+ parsedLines[parsedLines.length - 1] += `\n${previousLineCache.join(
+ "\n"
+ )}`;
+ previousLineCache = [];
+ }
+ let nextLine = "";
+ // Append cache to next line
+ if (nextLineCache.length) {
+ nextLine = nextLineCache.join("\n");
+ nextLineCache = [];
+ }
+ if (nextLine) {
+ nextLine += "\n";
+ }
+ nextLine += `${line}`;
+ parsedLines.push(nextLine);
+ }
+ }
+ }
+ return parsedLines;
+}
+
+function getLinesInNote(note: Zotero.Item): string[] {
+ if (!note) {
+ return [];
+ }
+ let noteText: string = note.getNote();
+ return parseHTMLLines(noteText);
+}
+
+async function setLinesToNote(note: Zotero.Item, lines: string[]) {
+ if (!note) {
+ return [];
+ }
+ let noteText: string = note.getNote();
+ let containerIndex = noteText.search(/data-schema-version="[0-9]*/g);
+ if (containerIndex === -1) {
+ note.setNote(`${lines.join("\n")}
`);
+ } else {
+ let noteHead = noteText.substring(0, containerIndex);
+ note.setNote(
+ `${noteHead}data-schema-version="8">${lines.join("\n")}`
+ );
+ }
+
+ await note.saveTx();
+}
+
+async function addLineToNote(
+ note: Zotero.Item,
+ html: string,
+ lineIndex: number = -1,
+ forceMetadata: boolean = false
+) {
+ if (!note || !html) {
+ return;
+ }
+ let noteLines = getLinesInNote(note);
+ if (lineIndex < 0 || lineIndex >= noteLines.length) {
+ lineIndex = noteLines.length;
+ }
+ ztoolkit.log(`insert to ${lineIndex}, it used to be ${noteLines[lineIndex]}`);
+ ztoolkit.log(html);
+
+ const editor = getEditorInstance(note.id);
+ if (editor && !forceMetadata) {
+ // The note is opened. Add line via note editor
+ const pos = getPositionAtLine(editor, lineIndex, "end");
+ ztoolkit.log("Add note line via note editor", pos);
+ insert(editor, html, pos);
+ // The selection is automatically moved to the next line
+ } else {
+ // The note editor does not exits yet. Fall back to modify the metadata
+ ztoolkit.log("Add note line via note metadata");
+ noteLines.splice(lineIndex, 0, html);
+ await setLinesToNote(note, noteLines);
+ }
+}
+
+async function renderNoteHTML(
+ html: string,
+ refNotes: Zotero.Item[]
+): Promise;
+async function renderNoteHTML(noteItem: Zotero.Item): Promise;
+async function renderNoteHTML(
+ htmlOrNote: string | Zotero.Item,
+ refNotes?: Zotero.Item[]
+): Promise {
+ let html: string;
+ if (typeof htmlOrNote === "string") {
+ html = htmlOrNote;
+ refNotes = (refNotes || []).filter((item) => item.isNote());
+ } else {
+ const noteItem = htmlOrNote as Zotero.Item;
+ if (!noteItem.isNote()) {
+ throw new Error("Item is not a note");
+ }
+ html = noteItem.getNote();
+ refNotes = [noteItem];
+ }
+
+ const parser = ztoolkit.getDOMParser();
+ let doc = parser.parseFromString(html, "text/html");
+ const imageAttachments = refNotes.reduce((acc, note) => {
+ acc.push(...Zotero.Items.get(note.getAttachments()));
+ return acc;
+ }, [] as Zotero.Item[]);
+
+ for (let attachment of imageAttachments) {
+ if (await attachment.fileExists()) {
+ let imageNodes = Array.from(
+ doc.querySelectorAll(`img[data-attachment-key="${attachment.key}"]`)
+ );
+ if (imageNodes.length) {
+ try {
+ const b64 = await getItemDataURL(attachment);
+ imageNodes.forEach((node) => node.setAttribute("src", b64));
+ } catch (e) {}
+ }
+ }
+ }
+
+ const bgNodes = doc.querySelectorAll(
+ "span[style]"
+ ) as NodeListOf;
+ for (let node of bgNodes) {
+ // Browser converts #RRGGBBAA hex color to rgba function, and we convert it to rgb function,
+ // because word processors don't understand colors with alpha channel
+ if (
+ node.style.backgroundColor &&
+ node.style.backgroundColor.startsWith("rgba")
+ ) {
+ node.style.backgroundColor =
+ node.style.backgroundColor
+ .replace("rgba", "rgb")
+ .split(",")
+ .slice(0, 3)
+ .join(",") + ")";
+ }
+ }
+ return doc.body.innerHTML;
+}
+
+function getNoteType(id: number) {
+ if (id === addon.data.workspace.mainId) {
+ return "main";
+ } else if (id === addon.data.workspace.previewId) {
+ return "preview";
+ } else {
+ return "default";
+ }
+}
+
+function getNoteTree(
+ note: Zotero.Item,
+ parseLink: boolean = true
+): TreeModel.Node {
+ const noteLines = getLinesInNote(note);
+ const parser = ztoolkit.getDOMParser();
+ let tree = new TreeModel();
+ let root = tree.parse({
+ id: -1,
+ level: 0,
+ lineIndex: -1,
+ endIndex: -1,
+ });
+ let id = 0;
+ let lastNode = root;
+ const headingRegex = new RegExp("^");
+ const linkRegex = new RegExp('href="(zotero://note/[^"]*)"');
+ for (let i in noteLines) {
+ let currentLevel = 7;
+ let lineElement = noteLines[i];
+ const matchHeadingResult = lineElement.match(headingRegex);
+ const matchLinkResult = parseLink ? lineElement.match(linkRegex) : null;
+ const isHeading = Boolean(matchHeadingResult);
+ // Links in blockquote are ignored
+ const isLink =
+ Boolean(matchLinkResult) && !noteLines[i].startsWith("= currentLevel) {
+ parentNode = parentNode.parent;
+ }
+
+ const currentNode = tree.parse({
+ id: id++,
+ level: currentLevel,
+ name: name,
+ lineIndex: parseInt(i),
+ endIndex: noteLines.length - 1,
+ link: link,
+ });
+ parentNode.addChild(currentNode);
+ const currentIndex = parentNode.children.indexOf(currentNode);
+ if (currentIndex > 0) {
+ const previousNode = parentNode.children[
+ currentIndex - 1
+ ] as TreeModel.Node;
+ // Traverse the previous node tree and set the end index
+ previousNode.walk((node) => {
+ if (node.model.endIndex > parseInt(i) - 1) {
+ node.model.endIndex = parseInt(i) - 1;
+ }
+ return true;
+ });
+ previousNode.model.endIndex = parseInt(i) - 1;
+ }
+ lastNode = currentNode;
+ }
+ }
+ return root;
+}
+
+function getNoteTreeFlattened(
+ note: Zotero.Item,
+ options: {
+ keepRoot?: boolean;
+ keepLink?: boolean;
+ customFilter?: (node: TreeModel.Node) => boolean;
+ } = { keepRoot: false, keepLink: false }
+): TreeModel.Node[] {
+ if (!note) {
+ return [];
+ }
+ return getNoteTree(note).all(
+ (node) =>
+ (options.keepRoot || node.model.lineIndex >= 0) &&
+ (options.keepLink || node.model.level <= 6) &&
+ (options.customFilter ? options.customFilter(node) : true)
+ );
+}
+
+function getNoteTreeNodeById(
+ note: Zotero.Item,
+ id: number,
+ root: TreeModel.Node | undefined = undefined
+) {
+ root = root || getNoteTree(note);
+ return root.first(function (node) {
+ return node.model.id === id;
+ });
+}
+
+function getNoteTreeNodesByLevel(
+ note: Zotero.Item,
+ level: number,
+ root: TreeModel.Node | undefined = undefined
+) {
+ root = root || getNoteTree(note);
+ return root.all(function (node) {
+ return node.model.level === level;
+ });
+}
+
+async function copyEmbeddedImagesFromNote(
+ targetNote: Zotero.Item,
+ sourceNotes: Zotero.Item[]
+) {
+ await Zotero.DB.executeTransaction(async () => {
+ for (const fromNote of sourceNotes) {
+ await Zotero.Notes.copyEmbeddedImages(fromNote, targetNote);
+ }
+ });
+}
+
+async function copyEmbeddedImagesInHTML(
+ html: string,
+ targetNote?: Zotero.Item,
+ refNotes: Zotero.Item[] = []
+) {
+ ztoolkit.log("parseEmbeddedImagesInHTML", arguments);
+ if (!targetNote) {
+ return html;
+ }
+
+ const attachments = refNotes.reduce((acc, note) => {
+ acc.push(...Zotero.Items.get(note.getAttachments()));
+ return acc;
+ }, [] as Zotero.Item[]);
+ if (!attachments.length) {
+ return html;
+ }
+
+ ztoolkit.log(attachments);
+
+ let doc = ztoolkit.getDOMParser().parseFromString(html, "text/html");
+
+ // Copy note image attachments and replace keys in the new note
+ for (let attachment of attachments) {
+ if (await attachment.fileExists()) {
+ let nodes = Array.from(
+ doc.querySelectorAll(`img[data-attachment-key="${attachment.key}"]`)
+ );
+ if (nodes.length) {
+ let copiedAttachment: Zotero.Item;
+ await Zotero.DB.executeTransaction(async () => {
+ Zotero.DB.requireTransaction();
+ copiedAttachment = await Zotero.Attachments.copyEmbeddedImage({
+ attachment,
+ note: targetNote,
+ });
+ });
+ nodes.forEach((node) =>
+ node.setAttribute("data-attachment-key", copiedAttachment.key)
+ );
+ }
+ }
+ }
+ ztoolkit.log("embed", doc.body.innerHTML);
+ return doc.body.innerHTML;
+}
+
+function dataURLtoBlob(dataurl: string) {
+ let parts = dataurl.split(",");
+ let matches = parts[0]?.match(/:(.*?);/);
+ if (!matches || !matches[1]) {
+ return;
+ }
+ let mime = matches[1];
+ if (parts[0].indexOf("base64") !== -1) {
+ let bstr = ztoolkit.getGlobal("atob")(parts[1]);
+ let n = bstr.length;
+ let u8arr = new Uint8Array(n);
+ while (n--) {
+ u8arr[n] = bstr.charCodeAt(n);
+ }
+
+ return new (ztoolkit.getGlobal("Blob"))([u8arr], {
+ type: mime,
+ });
+ }
+ return null;
+}
+
+async function importImageToNote(
+ note: Zotero.Item,
+ src: string,
+ type: "b64" | "url" | "file" = "b64"
+): Promise {
+ if (!note || !note.isNote()) {
+ return "";
+ }
+ let blob: Blob;
+ if (src.startsWith("data:")) {
+ const dataBlob = dataURLtoBlob(src);
+ if (!dataBlob) {
+ return;
+ }
+ blob = dataBlob;
+ } else if (type === "url") {
+ let res;
+ try {
+ res = await Zotero.HTTP.request("GET", src, { responseType: "blob" });
+ } catch (e) {
+ return;
+ }
+ blob = res.response;
+ } else if (type === "file") {
+ src = Zotero.File.normalizeToUnix(src);
+ const noteAttachmentKeys = Zotero.Items.get(note.getAttachments()).map(
+ (_i) => _i.key
+ );
+ const filename = src.split("/").pop()?.split(".").shift();
+ // The exported image is KEY.png by default.
+ // If it is already an attachment, just keep it.
+ if (noteAttachmentKeys.includes(filename || "")) {
+ return filename;
+ }
+ const imageData = await Zotero.File.getBinaryContentsAsync(src);
+ const array = new Uint8Array(imageData.length);
+ for (let i = 0; i < imageData.length; i++) {
+ array[i] = imageData.charCodeAt(i);
+ }
+ blob = new Blob([array], { type: "image/png" });
+ } else {
+ return;
+ }
+
+ let attachment = await Zotero.Attachments.importEmbeddedImage({
+ blob,
+ parentItemID: note.id,
+ saveOptions: {},
+ });
+
+ return attachment.key;
+}
diff --git a/src/utils/prefs.ts b/src/utils/prefs.ts
new file mode 100644
index 0000000..807277e
--- /dev/null
+++ b/src/utils/prefs.ts
@@ -0,0 +1,13 @@
+import { config } from "../../package.json";
+
+export function getPref(key: string) {
+ return Zotero.Prefs.get(`${config.prefsPrefix}.${key}`, true);
+}
+
+export function setPref(key: string, value: string | number | boolean) {
+ return Zotero.Prefs.set(`${config.prefsPrefix}.${key}`, value, true);
+}
+
+export function clearPref(key: string) {
+ return Zotero.Prefs.clear(`${config.prefsPrefix}.${key}`, true);
+}
diff --git a/src/utils/str.ts b/src/utils/str.ts
new file mode 100644
index 0000000..f8d96da
--- /dev/null
+++ b/src/utils/str.ts
@@ -0,0 +1,80 @@
+import seedrandom = require("seedrandom");
+
+export function slice(str: string, len: number) {
+ return str.length > len ? `${str.slice(0, len - 3)}...` : str;
+}
+
+export function fill(
+ str: string,
+ len: number,
+ options: { char: string; position: "start" | "end" } = {
+ char: " ",
+ position: "end",
+ }
+) {
+ if (str.length >= len) {
+ return str;
+ }
+ return str[options.position === "start" ? "padStart" : "padEnd"](
+ len - str.length,
+ options.char
+ );
+}
+
+export function formatPath(path: string, suffix: string = "") {
+ path = Zotero.File.normalizeToUnix(path);
+ if (Zotero.isWin) {
+ path = path.replace(/\\/g, "/");
+ path = OS.Path.join(...path.split(/\//));
+ }
+ if (Zotero.isMac && path.charAt(0) !== "/") {
+ path = "/" + path;
+ }
+ if (suffix && !path.endsWith(suffix)) {
+ path += suffix;
+ }
+ return path;
+}
+
+export async function getFileContent(path: string) {
+ const contentOrXHR = await Zotero.File.getContentsAsync(path);
+ const content =
+ typeof contentOrXHR === "string"
+ ? contentOrXHR
+ : (contentOrXHR as any as XMLHttpRequest).response;
+ return content;
+}
+
+export function randomString(len: number, seed?: string, chars?: string) {
+ if (!chars) {
+ chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
+ }
+ if (!len) {
+ len = 8;
+ }
+ let str = "";
+ const random: Function = seedrandom(seed);
+ for (let i = 0; i < len; i++) {
+ const rnum = Math.floor(random() * chars.length);
+ str += chars.substring(rnum, rnum + 1);
+ }
+ return str;
+}
+
+function arrayBufferToBase64(buffer: ArrayBufferLike) {
+ var binary = "";
+ var bytes = new Uint8Array(buffer);
+ var len = bytes.byteLength;
+ for (var i = 0; i < len; i++) {
+ binary += String.fromCharCode(bytes[i]);
+ }
+ return ztoolkit.getGlobal("btoa")(binary);
+}
+
+export async function getItemDataURL(item: Zotero.Item) {
+ let path = (await item.getFilePathAsync()) as string;
+ let buf = new Uint8Array((await OS.File.read(path, {})) as Uint8Array).buffer;
+ return (
+ "data:" + item.attachmentContentType + ";base64," + arrayBufferToBase64(buf)
+ );
+}
diff --git a/src/utils/wait.ts b/src/utils/wait.ts
new file mode 100644
index 0000000..db654b6
--- /dev/null
+++ b/src/utils/wait.ts
@@ -0,0 +1,35 @@
+export function waitUntil(
+ condition: () => boolean,
+ callback: () => void,
+ interval = 100,
+ timeout = 10000
+) {
+ const start = Date.now();
+ const intervalId = ztoolkit.getGlobal("setInterval")(() => {
+ if (condition()) {
+ ztoolkit.getGlobal("clearInterval")(intervalId);
+ callback();
+ } else if (Date.now() - start > timeout) {
+ ztoolkit.getGlobal("clearInterval")(intervalId);
+ }
+ }, interval);
+}
+
+export function waitUtilAsync(
+ condition: () => boolean,
+ interval = 100,
+ timeout = 10000
+) {
+ return new Promise((resolve, reject) => {
+ const start = Date.now();
+ const intervalId = ztoolkit.getGlobal("setInterval")(() => {
+ if (condition()) {
+ ztoolkit.getGlobal("clearInterval")(intervalId);
+ resolve();
+ } else if (Date.now() - start > timeout) {
+ ztoolkit.getGlobal("clearInterval")(intervalId);
+ reject();
+ }
+ }, interval);
+ });
+}
diff --git a/src/utils/window.ts b/src/utils/window.ts
new file mode 100644
index 0000000..0170eb1
--- /dev/null
+++ b/src/utils/window.ts
@@ -0,0 +1,31 @@
+import { getString } from "./locale";
+
+export { isWindowAlive, localeWindow };
+
+function isWindowAlive(win?: Window) {
+ return win && !Components.utils.isDeadWrapper(win) && !win.closed;
+}
+
+function localeWindow(win: Window) {
+ Array.from(win.document.querySelectorAll("*[locale-target]")).forEach(
+ (elem) => {
+ const errorInfo = "Locale Error";
+ const locales = elem.getAttribute("locale-target")?.split(",");
+ locales?.forEach((key) => {
+ const isProp = key in elem;
+ try {
+ const localeString = getString(
+ (isProp ? (elem as any)[key] : elem.getAttribute(key)).trim() || ""
+ );
+ isProp
+ ? ((elem as any)[key] = localeString)
+ : elem.setAttribute(key, localeString);
+ } catch (error) {
+ isProp
+ ? ((elem as any)[key] = errorInfo)
+ : elem.setAttribute(key, errorInfo);
+ }
+ });
+ }
+ );
+}
diff --git a/src/utils/workspace.ts b/src/utils/workspace.ts
new file mode 100644
index 0000000..09b5279
--- /dev/null
+++ b/src/utils/workspace.ts
@@ -0,0 +1,6 @@
+export enum OutlineType {
+ empty = 0,
+ treeView,
+ mindMap,
+ bubbleMap,
+}
diff --git a/src/wizard/wizardWindow.ts b/src/wizard/wizardWindow.ts
deleted file mode 100644
index bf3ecfd..0000000
--- a/src/wizard/wizardWindow.ts
+++ /dev/null
@@ -1,288 +0,0 @@
-/*
- * This file contains the first-run wizard window code.
- */
-
-import BetterNotes from "../addon";
-import { EditorMessage } from "../utils";
-import AddonBase from "../module";
-
-class WizardWindow extends AddonBase {
- enableSetup: boolean;
- enableCollection: boolean;
- collectionName: string;
- enableNote: boolean;
- template: string;
- templateCN: string;
- private _document: Document;
- constructor(parent: BetterNotes) {
- super(parent);
- this.enableSetup = true;
- this.enableCollection = true;
- this.collectionName = "";
- this.enableNote = true;
- this.template = `Zotero Better Notes User Guide: Workflow
-
Welcome to Zotero Better Notes !
-
This note helps you quickly learn how to use this addon in 3 min!
-
Let's start now.
-
View full documentation with images on GitHub: User Guide(EN) | User Guide(CN)
-
-
1 What is Better Notes
-
Better Notes is an extension of Zotero's built-in note function.
-
Zotero's note is much like a markdown/rich-text editor. You can edit the format with the tools above⬆️.
-
1.1 Workspace Window
-
The knowledge workspace window contains an outline area(left side⬅️), the main note area, and the preview area(right side➡️).
-
|---------------||----------------||----------------|
-
| || || |
-
| Outline || Main Note || Preview |
-
| || || |
-
|------------ --||---------------||----------------|
-
Open workspace by clicking the 'Open Workspace' line above the 'My Library' line in Zotero main window. Alternatively, open it with the '🏠home' button on the top-left of note editors.
-
Most functions are integrated in the upper left menu bar of the workspace window (in MacOS, the upper left of the screen), which will be described in subsequent chapters.
-
-
1.2 Main note
-
This addon uses a Zotero note item as the main note. It will show up on the main area of the workspace window.
-
All links will be added to the main note.
-
Click File -> Open Main Note in the workspace window to select a note as the main note.
-
-
2 Gather Ideas to Main Note
-
2.1 From Note
-
Select a note outside the workspace window(in Zotero items view or PDF viewer), you may realize a button with the addon's icon on the top of the note editor toolbar.
-
Click it, the current note link will be inserted into the main note's cursor position;
-
Select a heading, the note's link will be inserted into the end of this heading.
-
- 💡 Try it now!
- Open a PDF and open/create a note(in the right side bar of PDF viewer). Add a link below.
-
-
-
2.1.1 INSERT HERE
-
You can insert the link here.
-
-
2.2 From Annotation
-
You can find a button with the addon's icon on every annotation(in the left sidebar of PDF viewer).
-
Click it, and a new note with this annotation will be created under the PDF item. You can also add the link to the main note in the note editor.
-
- 💡 Try it now!
- Open a PDF and open/create an annotation(in the left sidebar of PDF viewer). Add a link.
-
-
-
3 Check Linked Notes in Workspace Window
-
3.1 View Linked Notes
-
Suppose you have added a lot of links to the main note. Now, it's time to view what you've got.
-
Go back to the workspace window.
-
Click links, the linked note will show up in the preview area(right side➡️).
-
- 💡 Try it now!
- Open a note link.
-
-
3.2 View Linked Note's PDF
-
Click the '📄PDF' button on the top-left of the preview area.
-
- 💡 Try it now!
- Open a linked note's PDF.
-
-
-
4 Outline Mode
-
Switch the outline mode with the '📊mode' button on the bottom of the outline area.
-
- 💡 Try it now!
- Try different outline modes.
-
-
-
5 LaTex Support
-
Removed since v0.7.0. Click the 'TEX' button on the tool bar of note editor to switch between LaTex view and editor view.
-
-
6 Export
-
Click the '⬆️export' button on the top-right of the main note area. Choose a format to export, e.g. MarkDown.
-
If you are using MS Word/OneNote, export to clipboard and paste there.
-
You can also choose to keep the notes in Zotero, and the contents of the notes will be synchronized through Zotero synchronization settings.
-
- 💡 Try it now!
- Export this main note!
-
-
-
7 Start a New Job
-
After the export, you may want to start a new job with a new empty main note.
-
Create a note and right-click to set it as the main note, or just create a new main note.
-
Switch between different main notes with the File -> Open Main Note/Create New Main Note .
-
- ✨ Hint
- Create a new collection and save all main notes there is the best way to manage them.
- The user guide should have done this for you.
-
-
-
Congratulations!
-
You can select a new main note and start working with Zotero Better Notes now. Have fun!
-
-
`;
- this.templateCN = `Zotero Better Notes 用户指引:工作流
-
欢迎使用 Zotero Better Notes !
-
本笔记帮助您在3分钟内快速学习如何使用此插件!
-
现在开始吧。
-
在GitHub上阅读带有图片的完整文档: User Guide(EN) | 用户指引(中文)
-
-
1 认识 Better Notes
-
Better Notes是Zotero内置note功能的扩展。
-
Zotero的note很像一个标记/富文本编辑器。您可以使用上方工具编辑格式⬆️。
-
1.1 工作区窗口
-
知识工作区窗口包含一个大纲区域(左侧⬅️),主笔记区域和预览区域(右侧➡️)。
-
|---------------||----------------||----------------|
-
| || || |
-
| 大纲 || 主笔记 || 预览 |
-
| || || |
-
|------------ --||---------------||----------------|
-
在Zotero主窗口中单击“我的文库”上方的“Open Workspace” 来打开工作区。或者,用笔记编辑器左上角的“🏠主页”按钮 。
-
工作区窗口的左上方菜单栏(在MacOS中,是屏幕左上角)中集成了大多数功能,将在后续章节进行介绍。
-
1.2 主笔记
-
这个插件使用某一个Zotero note作为主笔记。它将显示在工作区窗口的主笔记区域。
-
所有链接都将添加到主笔记中。
-
点击工作区窗口中的菜单栏 -> 文件 -> 打开主笔记 来选择不同的note作为主笔记。
-
-
2 在主笔记中收集想法
-
2.1 从Note
-
在工作区窗口外选择一个note(在Zotero 条目视图或PDF阅读器中),您会在笔记编辑器工具栏顶部看到一个带有本插件图标的按钮 。
-
点击它,当前笔记的链接将插入主笔记的光标位置;
-
选择一个标题层级,笔记的链接将插入该标题的末尾。
-
- 💡 尝试一下!
- 打开PDF并打开/创建笔记(在PDF 阅读器的右侧栏中)。用上面的方法在这条主笔记添加一个链接。
-
-
-
2.1.1 用来插入链接的位置
-
你可以在这里插入链接。
-
-
2.2 从Annotation(批注高亮)
-
你可以在每个批注上找到一个带有插件图标的按钮 (在PDF 阅读器的左侧栏中)。
-
单击它,PDF项目下将创建一个带有此批注的新笔记。也可以在打开的笔记编辑器中将链接添加到主笔记。
-
- 💡 尝试一下!
- 打开PDF并打开/创建批注高亮(在PDF 阅读器的左侧栏中)。用上面的方法在这条主笔记添加一个链接。
-
-
-
3 查看工作区窗口中的链接笔记
-
3.1 查看链接笔记
-
假设你已经在主笔记添加了很多的链接。现在,是时候看看你的结果了。
-
返回工作区窗口。
-
单击链接,链接的笔记将显示在预览区域(右侧➡️)。
-
- 💡 尝试一下!
- 在工作区窗口打开一个笔记链接。
-
-
3.2 查看链接笔记的 PDF
-
在上一步打开的预览笔记中,点击预览区左上角的“📄PDF”按钮 。
-
- 💡尝试一下!
- 打开一个链接笔记的 PDF。
-
-
-
4 大纲视图
-
点击大纲区域左下角的 ‘📊大纲模式‘ 按钮 来切换大纲视图模式。
-
- 💡 尝试一下!
- 尝试不同的大纲模式(思维导图)
-
-
-
5 LaTex支持
-
Removed since v0.7.0. 点击笔记编辑器工具栏中'TEX'按钮来切换预览和编辑模式。
-
-
6 导出
-
点击主笔记区域右上角的“⬆️导出”按钮 。选择要导出的格式,比如MarkDown。
-
如果您使用的是MS Word/OneNote,请导出到剪贴板并粘贴到那里。
-
- 💡 尝试一下!
- 导出这个主笔记!
-
-
-
7 开始新的任务
-
导出后,您可能希望使用新的空主笔记开始新任务。
-
创建一个note笔记,然后右键单击将其设置为主笔记;或者直接创建一个新的主笔记。
-
使用菜单栏中的文件->打开主笔记/创建新主笔记 来切换不同的主笔记。
-
- ✨ 提示
- 创建一个新的文件夹并在其中专门保存所有的主笔记——这是管理主笔记的最佳方法。
- 用户指引应该已经为您做到了这一点。
-
-
-
恭喜!
-
你现在可以选择或新建一个主笔记,然后开始使用 Zotero Better Notes 了。用的开心!
-
-
`;
- }
- init(_document: Document) {
- this._document = _document;
- this._Addon.toolkit.Tool.log("Initialize AddonWizard.")
- this.updateCollectionSetup();
- }
- changeSetup() {
- this.enableSetup = (
- this._document.getElementById(
- "Knowledge4Zotero-setup-enable"
- ) as XUL.Checkbox
- ).checked;
- (
- this._document.getElementById(
- "Knowledge4Zotero-setup-collectionenable"
- ) as XUL.Checkbox
- ).disabled = !this.enableSetup;
- (
- this._document.getElementById(
- "Knowledge4Zotero-setup-collectionname"
- ) as XUL.Textbox
- ).disabled = !(this.enableSetup && this.enableCollection);
- (
- this._document.getElementById(
- "Knowledge4Zotero-setup-noteenable"
- ) as XUL.Checkbox
- ).disabled = !this.enableSetup;
- }
- updateCollectionSetup() {
- this.enableCollection = (
- this._document.getElementById(
- "Knowledge4Zotero-setup-collectionenable"
- ) as XUL.Checkbox
- ).checked;
- this.collectionName = (
- this._document.getElementById(
- "Knowledge4Zotero-setup-collectionname"
- ) as XUL.Textbox
- ).value;
- (
- this._document.getElementById(
- "Knowledge4Zotero-setup-collectionname"
- ) as XUL.Textbox
- ).disabled = !(this.enableSetup && this.enableCollection);
- }
- updateNoteSetup() {
- this.enableNote = (
- this._document.getElementById(
- "Knowledge4Zotero-setup-noteenable"
- ) as XUL.Checkbox
- ).checked;
- }
- async setup() {
- if (this.enableSetup) {
- if (this.enableCollection && this.collectionName.trim().length > 0) {
- let collection = Zotero.Collections.getLoaded()
- .filter((c) => !c.parentID)
- .find((c) => c.name === this.collectionName);
- if (!collection) {
- collection = new Zotero.Collection();
- collection.name = this.collectionName;
- await collection.saveTx();
- }
- }
- if (this.enableNote) {
- const noteID = await ZoteroPane.newNote();
- (Zotero.Items.get(noteID) as Zotero.Item).setNote(
- Zotero.locale === "zh-CN" ? this.templateCN : this.template
- );
- await this._Addon.ZoteroEvents.onEditorEvent(
- new EditorMessage("setMainNote", {
- params: { itemID: noteID, enableConfirm: false, enableOpen: true },
- })
- );
- }
- }
- }
-}
-
-export default WizardWindow;
diff --git a/src/workspace/workspaceMenu.ts b/src/workspace/workspaceMenu.ts
deleted file mode 100644
index cb6c97d..0000000
--- a/src/workspace/workspaceMenu.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * This file contains workspace menu related code.
- */
-
-import BetterNotes from "../addon";
-import { OutlineType } from "../utils";
-import AddonBase from "../module";
-
-class WorkspaceMenu extends AddonBase {
- constructor(parent: BetterNotes) {
- super(parent);
- }
-
- public getWorkspaceMenuWindow(): Window {
- return this._Addon.WorkspaceWindow.workspaceTabId
- ? this._Addon.WorkspaceWindow.workspaceTabId !== "WINDOW"
- ? window
- : this._Addon.WorkspaceWindow.getWorkspaceWindow()
- : window;
- }
-
- public updateViewMenu() {
- Zotero.debug(
- `updateViewMenu, ${this._Addon.WorkspaceOutline.currentOutline}`
- );
- const _mainWindow = this.getWorkspaceMenuWindow();
- const treeview = _mainWindow.document.getElementById("menu_treeview");
- this._Addon.WorkspaceOutline.currentOutline === OutlineType.treeView
- ? treeview.setAttribute("checked", true as any)
- : treeview.removeAttribute("checked");
- const mindmap = _mainWindow.document.getElementById("menu_mindmap");
- this._Addon.WorkspaceOutline.currentOutline === OutlineType.mindMap
- ? mindmap.setAttribute("checked", true as any)
- : mindmap.removeAttribute("checked");
- const bubblemap = _mainWindow.document.getElementById("menu_bubblemap");
- this._Addon.WorkspaceOutline.currentOutline === OutlineType.bubbleMap
- ? bubblemap.setAttribute("checked", true as any)
- : bubblemap.removeAttribute("checked");
-
- const noteFontSize = Zotero.Prefs.get("note.fontSize");
- for (let menuitem of this._Addon.WorkspaceWindow.workspaceWindow.document.querySelectorAll(
- `#note-font-size-menu menuitem`
- )) {
- if (parseInt(menuitem.getAttribute("label")) == noteFontSize) {
- menuitem.setAttribute("checked", true as any);
- } else {
- menuitem.removeAttribute("checked");
- }
- }
- }
-}
-
-export default WorkspaceMenu;
diff --git a/src/workspace/workspaceOutline.ts b/src/workspace/workspaceOutline.ts
deleted file mode 100644
index ff21b28..0000000
--- a/src/workspace/workspaceOutline.ts
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- * This file contains workspace ontline related code.
- */
-
-import BetterNotes from "../addon";
-import { OutlineType } from "../utils";
-import AddonBase from "../module";
-
-class WorkspaceOutline extends AddonBase {
- public currentOutline: OutlineType;
- public currentNodeID: number;
-
- constructor(parent: BetterNotes) {
- super(parent);
- this.currentOutline = OutlineType.treeView;
- this.currentNodeID = -1;
- }
-
- public switchView(newType: OutlineType = undefined) {
- if (!newType) {
- newType = this.currentOutline + 1;
- }
- if (newType > OutlineType.bubbleMap) {
- newType = OutlineType.treeView;
- }
- this._Addon.WorkspaceWindow.workspaceWindow.document.getElementById(
- "outline-saveImage"
- ).hidden = newType === OutlineType.treeView;
- this._Addon.WorkspaceWindow.workspaceWindow.document.getElementById(
- "outline-saveFreeMind"
- ).hidden = newType === OutlineType.treeView;
- const mindmap =
- this._Addon.WorkspaceWindow.workspaceWindow.document.getElementById(
- "mindmap-container"
- );
-
- const oldIframe =
- this._Addon.WorkspaceWindow.workspaceWindow.document.getElementById(
- "mindmapIframe"
- );
- if (oldIframe) {
- oldIframe.remove();
- }
- this.currentOutline = newType;
- const srcList = [
- "",
- "chrome://Knowledge4Zotero/content/treeView.html",
- "chrome://Knowledge4Zotero/content/mindMap.html",
- "chrome://Knowledge4Zotero/content/bubbleMap.html",
- ];
- const iframe = this._Addon.toolkit.UI.createElement(
- this._Addon.WorkspaceWindow.workspaceWindow.document,
- "iframe",
- "xul"
- ) as XUL.Element;
- iframe.setAttribute("id", "mindmapIframe");
- iframe.setAttribute("src", srcList[this.currentOutline]);
- mindmap.append(iframe);
- this.resizeOutline();
- this.updateOutline();
- // Clear stored node id
- this.currentNodeID = -1;
- this._Addon.WorkspaceMenu.updateViewMenu();
- }
-
- public async updateOutline() {
- this._Addon.toolkit.Tool.log("updateMindMap")
- // await this._initIframe.promise;
- const _window = this._Addon.WorkspaceWindow.getWorkspaceWindow();
- if (!_window) {
- return;
- }
- const iframe = _window.document.getElementById(
- "mindmapIframe"
- ) as HTMLIFrameElement;
- iframe.contentWindow.postMessage(
- {
- type: "setMindMapData",
- nodes: this._Addon.NoteUtils.getNoteTreeAsList(
- this._Addon.WorkspaceWindow.getWorkspaceNote(),
- true,
- false
- ),
- },
- "*"
- );
- }
-
- public saveImage() {
- this._Addon.toolkit.Tool.log("saveImage")
- const _window = this._Addon.WorkspaceWindow.getWorkspaceWindow();
- if (!_window) {
- return;
- }
- const iframe = _window.document.getElementById(
- "mindmapIframe"
- ) as HTMLIFrameElement;
- iframe.contentWindow.postMessage(
- {
- type: "saveSVG",
- },
- "*"
- );
- }
-
- public resizeOutline() {
- const iframe =
- this._Addon.WorkspaceWindow.workspaceWindow.document.getElementById(
- "mindmapIframe"
- );
- const container =
- this._Addon.WorkspaceWindow.workspaceWindow.document.getElementById(
- "zotero-knowledge-outline"
- );
- if (iframe) {
- iframe.style.height = `${container.clientHeight - 60}px`;
- iframe.style.width = `${container.clientWidth - 10}px`;
- }
- }
-}
-
-export default WorkspaceOutline;
diff --git a/src/workspace/workspaceWindow.ts b/src/workspace/workspaceWindow.ts
deleted file mode 100644
index d01c1d8..0000000
--- a/src/workspace/workspaceWindow.ts
+++ /dev/null
@@ -1,393 +0,0 @@
-/*
- * This file contains workspace window related code.
- */
-
-import BetterNotes from "../addon";
-import { EditorMessage, OutlineType } from "../utils";
-import AddonBase from "../module";
-
-class WorkspaceWindow extends AddonBase {
- private _initIframe: _ZoteroTypes.PromiseObject;
- public workspaceWindow: Window;
- public workspaceTabId: string;
- public workspaceNoteEditor: Zotero.EditorInstance | undefined;
- public previewItemID: number;
- private _firstInit: boolean;
- public _workspacePromise: _ZoteroTypes.PromiseObject;
- private _DOMParser: any;
-
- constructor(parent: BetterNotes) {
- super(parent);
- this._initIframe = Zotero.Promise.defer();
- this.workspaceTabId = "";
- this._firstInit = true;
- }
-
- public getWorkspaceWindow(): Window | undefined {
- if (this.workspaceWindow && !this.workspaceWindow.closed) {
- return this.workspaceWindow;
- }
- return undefined;
- }
-
- async openWorkspaceWindow(
- type: "window" | "tab" = "tab",
- reopen: boolean = false,
- select: boolean = true
- ) {
- if (this.getWorkspaceWindow()) {
- if (!reopen) {
- this._Addon.toolkit.Tool.log("openWorkspaceWindow: focus");
- if (this.workspaceTabId !== "WINDOW") {
- Zotero_Tabs.select(this.workspaceTabId);
- } else {
- (this.getWorkspaceWindow() as Window).focus();
- }
- return;
- } else {
- this._Addon.toolkit.Tool.log("openWorkspaceWindow: reopen");
- this.closeWorkspaceWindow();
- }
- }
- this._DOMParser = window.DOMParser;
- this._workspacePromise = Zotero.Promise.defer();
- this._firstInit = true;
- if (type === "window") {
- this._Addon.toolkit.Tool.log("openWorkspaceWindow: as window");
- this._initIframe = Zotero.Promise.defer();
- let win = window.open(
- "chrome://Knowledge4Zotero/content/workspace.xul",
- "_blank",
- "chrome,extrachrome,menubar,resizable,scrollbars,status,width=1000,height=600"
- );
- this.workspaceWindow = win as Window;
- this.workspaceTabId = "WINDOW";
- await this.waitWorkspaceReady();
- this.setWorkspaceNote("main");
- this._Addon.NoteUtils.currentLine[this.getWorkspaceNote().id] = -1;
- this.initKnowledgeWindow();
- this._Addon.WorkspaceOutline.switchView(OutlineType.treeView);
- this._Addon.WorkspaceOutline.updateOutline();
- this._Addon.ZoteroViews.updateAutoInsertAnnotationsMenu();
- } else {
- this._Addon.toolkit.Tool.log("openWorkspaceWindow: as tab");
- this._initIframe = Zotero.Promise.defer();
- // Avoid sidebar show up
- Zotero_Tabs.jump(0);
- let { id, container } = Zotero_Tabs.add({
- type: "betternotes",
- title: this._Addon.Locale.getString("library.workspace"),
- index: 1,
- data: {},
- select: select,
- onClose: () => (this.workspaceTabId = ""),
- });
- this.workspaceTabId = id;
- const _iframe = this._Addon.toolkit.UI.createElement(
- document,
- "browser",
- "xul"
- ) as XUL.Element;
- _iframe.setAttribute("class", "reader");
- _iframe.setAttribute("flex", "1");
- _iframe.setAttribute("type", "content");
- _iframe.setAttribute(
- "src",
- "chrome://Knowledge4Zotero/content/workspace.xul"
- );
- container.appendChild(_iframe);
-
- // @ts-ignore
- this.workspaceWindow = _iframe.contentWindow;
- await this.waitWorkspaceReady();
-
- this._Addon.ZoteroViews.hideMenuBar(this.workspaceWindow.document);
-
- this._Addon.NoteUtils.currentLine[this.getWorkspaceNote().id] = -1;
- this.initKnowledgeWindow();
- this._Addon.WorkspaceOutline.switchView(OutlineType.treeView);
- this._Addon.WorkspaceOutline.updateOutline();
- }
- // DONT TOUCH
- // Avoid DOMParser raises error because of re-loading chrome://zotero/content/include.js
- // in workspace.xul
- window.DOMParser = this._DOMParser;
- }
-
- public closeWorkspaceWindow() {
- if (this.getWorkspaceWindow()) {
- if (this.workspaceTabId !== "WINDOW") {
- Zotero_Tabs.close(this.workspaceTabId);
- } else {
- (this.getWorkspaceWindow() as Window).close();
- }
- }
- this.workspaceTabId = "";
- }
-
- public async waitWorkspaceReady() {
- let _window = this.getWorkspaceWindow() as Window;
- if (!_window) {
- return false;
- }
- let t = 0;
- await this._workspacePromise.promise;
- return true;
- }
-
- public initKnowledgeWindow() {
- this.workspaceWindow.addEventListener(
- "message",
- (e) => this.messageHandler(e),
- false
- );
- this._Addon.WorkspaceOutline.currentOutline = OutlineType.treeView;
- this.workspaceWindow.document
- .getElementById("outline-switchview")
- .addEventListener("click", async (e) => {
- this._Addon.WorkspaceOutline.switchView();
- });
- this.workspaceWindow.document
- .getElementById("outline-saveImage")
- .addEventListener("click", (e) => {
- this._Addon.WorkspaceOutline.saveImage();
- });
- this.workspaceWindow.document
- .getElementById("outline-saveFreeMind")
- .addEventListener("click", (e) => {
- this._Addon.NoteExport.exportNote(this.getWorkspaceNote(), {
- exportFreeMind: true,
- });
- });
- this.workspaceWindow.addEventListener("resize", (e) =>
- this._Addon.WorkspaceOutline.resizeOutline()
- );
- this.workspaceWindow.document
- .getElementById("outline-splitter")
- .addEventListener("mouseup", async (e) => {
- this._Addon.WorkspaceOutline.resizeOutline();
- });
- this.workspaceWindow.addEventListener("close", (e) => {
- this.workspaceWindow = undefined;
- this.workspaceTabId = "";
- this._Addon.ZoteroViews.updateAutoInsertAnnotationsMenu();
- });
- }
-
- private async messageHandler(e) {
- this._Addon.toolkit.Tool.log(`view message ${e.data.type}`);
- if (e.data.type === "ready") {
- this._initIframe.resolve();
- } else if (e.data.type === "getMindMapData") {
- this._Addon.WorkspaceOutline.updateOutline();
- } else if (e.data.type === "jumpNode") {
- this._Addon.ZoteroEvents.onEditorEvent(
- new EditorMessage("jumpNode", {
- params: e.data,
- })
- );
- } else if (e.data.type === "jumpNote") {
- this._Addon.toolkit.Tool.log(e.data);
- this._Addon.ZoteroEvents.onEditorEvent(
- new EditorMessage("onNoteLink", {
- params: await this._Addon.NoteUtils.getNoteFromLink(e.data.link),
- })
- );
- } else if (e.data.type === "moveNode") {
- await this._Addon.NoteUtils.moveNode(
- e.data.fromID,
- e.data.toID,
- e.data.moveType
- );
- } else if (e.data.type === "saveSVGReturn") {
- this._Addon.toolkit.Tool.log(e.data.image);
- const filename = await this._Addon.toolkit.Tool.openFilePicker(
- `${Zotero.getString("fileInterface.export")} SVG Image`,
- "save",
- [["SVG File(*.svg)", "*.svg"]],
- `${this._Addon.WorkspaceWindow.getWorkspaceNote().getNoteTitle()}.svg`
- );
- if (filename) {
- await Zotero.File.putContentsAsync(
- this._Addon.NoteUtils.formatPath(filename),
- e.data.image
- );
- this._Addon.ZoteroViews.showProgressWindow(
- "Better Notes",
- `Image Saved to ${filename}`
- );
- }
- }
- }
-
- public async getWorkspaceEditor(type: "main" | "preview" = "main") {
- let _window = this._Addon.WorkspaceWindow.getWorkspaceWindow() as Window;
- if (!_window) {
- return;
- }
- await this._Addon.WorkspaceWindow.waitWorkspaceReady();
- return _window.document.getElementById(`zotero-note-editor-${type}`);
- }
-
- getWorkspaceNote(): Zotero.Item {
- return Zotero.Items.get(
- Zotero.Prefs.get("Knowledge4Zotero.mainKnowledgeID") as number
- ) as Zotero.Item;
- }
-
- async getWorkspaceEditorInstance(
- type: "main" | "preview" = "main",
- wait: boolean = true
- ): Promise {
- let noteEditor = (await this.getWorkspaceEditor(type)) as any;
- if (!noteEditor) {
- return;
- }
- let t = 0;
- while (wait && !noteEditor.getCurrentInstance() && t < 500) {
- t += 1;
- await Zotero.Promise.delay(10);
- }
- this.workspaceNoteEditor =
- noteEditor.getCurrentInstance() as Zotero.EditorInstance;
- return this.workspaceNoteEditor;
- }
-
- getEditorInstance(note: Zotero.Item) {
- // If there are multiple editors of main note available, we use the workspace editor.
- if (
- note.id === this.getWorkspaceNote().id &&
- this.getWorkspaceWindow() &&
- this.workspaceNoteEditor &&
- !Components.utils.isDeadWrapper(this.workspaceNoteEditor._iframeWindow)
- ) {
- return this.workspaceNoteEditor;
- }
- const editor = (
- Zotero.Notes._editorInstances as Zotero.EditorInstance[]
- ).find(
- (e) =>
- e._item.id === note.id &&
- !Components.utils.isDeadWrapper(e._iframeWindow)
- );
- if (note.id === this.getWorkspaceNote().id) {
- this.workspaceNoteEditor = editor;
- }
- return editor;
- }
-
- async setWorkspaceNote(
- type: "main" | "preview" = "main",
- note: Zotero.Item | undefined = undefined,
- showPopup: boolean = true
- ) {
- let _window = this.getWorkspaceWindow() as Window;
- note = note || this.getWorkspaceNote();
- if (type === "preview") {
- if (!_window) {
- return;
- }
- const splitter = _window.document.getElementById("preview-splitter");
- splitter && splitter.setAttribute("state", "open");
- this.previewItemID = note.id;
- } else {
- // Set line to default
- this._Addon.NoteUtils.currentLine[note.id] = -1;
- if (Zotero.Prefs.get("Knowledge4Zotero.mainKnowledgeID") !== note.id) {
- Zotero.Prefs.set("Knowledge4Zotero.mainKnowledgeID", note.id);
- }
- if (
- !note.isEditable() ||
- // @ts-ignore
- note.deleted ||
- // @ts-ignore
- (note.parentItem && note.parentItem.deleted)
- ) {
- this._Addon.ZoteroViews.showProgressWindow(
- "Better Notes",
- "The main note is readonly, because it's trashed or belongs to a readonly group library.",
- "default",
- 20000
- );
- }
- }
- await this.waitWorkspaceReady();
- let noteEditor: any = await this.getWorkspaceEditor(type);
- if (noteEditor) {
- if (!noteEditor._initialized) {
- noteEditor._iframe.contentWindow.addEventListener(
- "drop",
- (event) => {
- noteEditor._iframe.contentWindow.wrappedJSObject.droppedData =
- Components.utils.cloneInto(
- {
- "text/plain": event.dataTransfer.getData("text/plain"),
- "text/html": event.dataTransfer.getData("text/html"),
- "zotero/annotation":
- event.dataTransfer.getData("zotero/annotation"),
- "zotero/item": event.dataTransfer.getData("zotero/item"),
- },
- noteEditor._iframe.contentWindow
- );
- },
- true
- );
- noteEditor._initialized = true;
- }
- noteEditor.mode = "edit";
- noteEditor.viewMode = "library";
- noteEditor.parent = null;
- noteEditor.item = note;
- if (!noteEditor || !noteEditor.getCurrentInstance()) {
- await noteEditor.initEditor();
- }
-
- await noteEditor._editorInstance._initPromise;
- const position = (
- this._Addon.EditorViews.getEditorElement(
- noteEditor._editorInstance._iframeWindow.document
- ).parentNode as HTMLElement
- ).scrollTop;
- // Due to unknown reasons, only after the second init the editor will be correctly loaded.
- // Thus we must init it twice
- if (this._firstInit) {
- this._firstInit = false;
- await noteEditor.initEditor();
- }
- this._Addon.EditorViews.scrollToPosition(
- noteEditor._editorInstance,
- position
- );
- }
-
- if (type === "main") {
- this._Addon.WorkspaceOutline.updateOutline();
- this._Addon.ZoteroViews.updateWordCount();
- const recentMainNotes = Zotero.Items.get(
- new Array(
- ...new Set(
- (
- Zotero.Prefs.get("Knowledge4Zotero.recentMainNoteIds") as string
- ).split(",")
- )
- )
- ) as Zotero.Item[];
- recentMainNotes.splice(0, 0, note);
- Zotero.Prefs.set(
- "Knowledge4Zotero.recentMainNoteIds",
- new Array(...new Set(recentMainNotes.map((item) => String(item.id))))
- .slice(0, 10)
- .filter((id) => id)
- .join(",")
- );
- if (showPopup) {
- this._Addon.ZoteroViews.showProgressWindow(
- "Better Notes",
- `Set main Note to: ${note.getNoteTitle()}`
- );
- }
- }
- }
-}
-
-export default WorkspaceWindow;
diff --git a/src/zotero/events.ts b/src/zotero/events.ts
deleted file mode 100644
index 882e067..0000000
--- a/src/zotero/events.ts
+++ /dev/null
@@ -1,1110 +0,0 @@
-/*
- * This file contains the life-time and UI events.
- */
-
-import BetterNotes from "../addon";
-import { EditorMessage } from "../utils";
-import AddonBase from "../module";
-import { addonName } from "../../package.json";
-
-class ZoteroEvents extends AddonBase {
- constructor(parent: BetterNotes) {
- super(parent);
- }
-
- public async onInit() {
- const development = "development";
- const production = "production";
- // The env will be replaced after esbuild
- // @ts-ignore
- this._Addon.env = __env__;
- this._Addon.toolkit.Tool.logOptionsGlobal.prefix = `[${addonName}]`;
- this._Addon.toolkit.Tool.logOptionsGlobal.disableConsole =
- this._Addon.env === "production";
- this._Addon.toolkit.Tool.log("init called");
- this._Addon.Locale.initLocale();
- this.initProxyHandler();
-
- this.addEditorInstanceListener();
- this._Addon.ZoteroNotifies.initNotifyCallback();
-
- await Zotero.uiReadyPromise;
- this._Addon.ZoteroViews.addOpenWorkspaceButton();
- this._Addon.ZoteroViews.addNewMainNoteButton();
-
- if (!Zotero.Prefs.get("Knowledge4Zotero.mainKnowledgeID")) {
- this.onEditorEvent(new EditorMessage("openUserGuide", {}));
- }
- this.resetState();
-
- this.initWorkspaceTab();
- this._Addon.ZoteroViews.keppDefaultMenuOrder();
- this._Addon.ZoteroViews.switchRealMenuBar(true);
- this._Addon.ZoteroViews.switchKey(true);
-
- this.initItemSelectListener();
-
- // Set a init sync
- this._Addon.SyncController.setSync();
- }
-
- private initProxyHandler() {
- const openNoteExtension = {
- noContent: true,
- doAction: async (uri: any) => {
- let message = {
- type: "onNoteLink",
- content: {
- params: await this._Addon.NoteUtils.getNoteFromLink(uri.spec),
- },
- };
- await this._Addon.ZoteroEvents.onEditorEvent(message);
- },
- newChannel: function (uri: any) {
- this.doAction(uri);
- },
- };
- Services.io.getProtocolHandler("zotero").wrappedJSObject._extensions[
- "zotero://note"
- ] = openNoteExtension;
- }
-
- private async initWorkspaceTab() {
- let state = Zotero.Session.state.windows.find((x) => x.type === "pane");
- this._Addon.toolkit.Tool.log("initWorkspaceTab");
- this._Addon.toolkit.Tool.log(state);
- if (state) {
- const noteTab = state.tabs.find((t) => t.type === "betternotes");
- this._Addon.toolkit.Tool.log(noteTab);
- if (noteTab) {
- let t = 0;
- while (t < 5) {
- t += 1;
- try {
- await this._Addon.WorkspaceWindow.openWorkspaceWindow(
- "tab",
- false,
- false
- );
- break;
- } catch (e) {
- this._Addon.ZoteroViews.showProgressWindow(
- "Recovering Note Workspace Failed",
- e
- );
- this._Addon.toolkit.Tool.log(e);
- }
- await Zotero.Promise.delay(1000);
- }
- }
- }
- }
-
- private initItemSelectListener() {
- ZoteroPane.itemsView.onSelect.addListener(() => {
- const items = ZoteroPane.getSelectedItems();
- const hasNote = items.filter((i) => i.isNote()).length > 0;
- const singleItem = items.length === 1;
- document
- .querySelectorAll(".popup-type-single")
- .forEach((el) => ((el as HTMLElement).hidden = !singleItem));
- document
- .querySelectorAll(".popup-type-multiple")
- .forEach((el) => ((el as HTMLElement).hidden = singleItem));
- document
- .querySelectorAll(".popup-type-single-note")
- .forEach(
- (el) => ((el as HTMLElement).hidden = !(singleItem && hasNote))
- );
- });
- }
-
- private async onEditorInstanceCreated(instance: Zotero.EditorInstance) {
- await instance._initPromise;
- instance._knowledgeUIInitialized = false;
-
- // item.getNote may not be initialized yet
- if (Zotero.ItemTypes.getID("note") !== instance._item.itemTypeID) {
- return;
- }
-
- this._Addon.toolkit.Tool.log("note editor initializing...");
- await this._Addon.EditorViews.initEditor(instance);
- this._Addon.toolkit.Tool.log("note editor initialized.");
-
- const currentID = instance._item.id;
-
- if (
- instance._iframeWindow.document.body.getAttribute(
- "betternotes-status"
- ) !== String(currentID)
- ) {
- // Put event listeners here to access Zotero instance
- instance._iframeWindow.document.addEventListener(
- "selectionchange",
- async (e: Event) => {
- e.stopPropagation();
- e.preventDefault();
- await this._Addon.NoteUtils.onSelectionChange(
- instance,
- parseInt(
- instance._iframeWindow.document.body.getAttribute(
- "betternotes-status"
- )
- )
- );
- }
- );
- instance._iframeWindow.document.addEventListener("click", async (e) => {
- if (
- (e.target as HTMLElement).tagName === "IMG" &&
- e.ctrlKey &&
- (Zotero.Prefs.get(
- "Knowledge4Zotero.imagePreview.ctrlclick"
- ) as boolean)
- ) {
- openPreview(e);
- }
- if ((e.target as HTMLElement).tagName === "A") {
- const link = (e.target as HTMLLinkElement).href;
- const actions = {
- // @ts-ignore
- openLink: () => window.openURL(link),
- openLinkIfNote: () => {
- link.includes("zotero://note") ? actions.openLink() : null;
- },
- openLinkIfSelect: () => {
- link.includes("zotero://select") ? actions.openLink() : null;
- },
- openLinkIfPDF: () => {
- link.includes("zotero://open-pdf") ? actions.openLink() : null;
- },
- openLinkInNewWindow: async () => {
- if (link.includes("zotero://note")) {
- ZoteroPane.openNoteWindow(
- (
- (await this._Addon.NoteUtils.getNoteFromLink(link))
- .item as Zotero.Item
- )?.id
- );
- } else {
- actions.openLink();
- }
- },
- copyLink: async () => {
- this._Addon.toolkit.Tool.getCopyHelper()
- .addText(link, "text/unicode")
- .addText((e.target as HTMLLinkElement).outerHTML, "text/html")
- .copy();
- },
- setMainNote: async () => {
- const noteItem = (
- await this._Addon.NoteUtils.getNoteFromLink(link)
- ).item as Zotero.Item;
- if (!noteItem) {
- return;
- }
- await this.onEditorEvent(
- new EditorMessage("setMainNote", {
- params: {
- itemID: noteItem.id,
- enableConfirm: false,
- enableOpen: true,
- },
- })
- );
- },
- };
- const shiftAction = Zotero.Prefs.get(
- "Knowledge4Zotero.linkAction.shiftclick"
- ) as string;
- const ctrlAction = Zotero.Prefs.get(
- "Knowledge4Zotero.linkAction.ctrlclick"
- ) as string;
- const altAction = Zotero.Prefs.get(
- "Knowledge4Zotero.linkAction.altclick"
- ) as string;
- const clickAction = Zotero.Prefs.get(
- "Knowledge4Zotero.linkAction.click"
- ) as string;
- if (e.shiftKey && shiftAction) {
- actions[shiftAction]();
- } else if (e.ctrlKey && ctrlAction) {
- actions[ctrlAction]();
- } else if (e.altKey && altAction) {
- actions[altAction]();
- } else if (clickAction && !(e.shiftKey || e.ctrlKey || e.altKey)) {
- actions[clickAction]();
- }
- }
- });
- const openPreview = (e: MouseEvent) => {
- const imgs = instance._iframeWindow.document
- .querySelector(".primary-editor")
- ?.querySelectorAll("img");
- this._Addon.EditorImageViewer.onInit(
- Array.prototype.map.call(imgs, (e: HTMLImageElement) => e.src),
- Array.prototype.indexOf.call(imgs, e.target),
- instance._item.getNoteTitle(),
- this._Addon.EditorImageViewer.pined
- );
- };
- instance._iframeWindow.document.addEventListener("dblclick", (e) => {
- if (
- (e.target as HTMLElement).tagName === "IMG" &&
- (Zotero.Prefs.get(
- "Knowledge4Zotero.imagePreview.ctrlclick"
- ) as Boolean)
- ) {
- openPreview(e);
- }
- });
- instance._iframeWindow.document.body.setAttribute(
- "betternotes-status",
- String(instance._item.id)
- );
- }
-
- instance._popup.addEventListener("popupshowing", (e) => {
- if (e.originalTarget !== instance._popup) {
- return;
- }
- this._Addon.EditorViews.updatePopupMenu();
- });
-
- instance._iframeWindow.addEventListener("mousedown", (e) => {
- this._Addon.EditorController.activeEditor = instance;
- });
-
- instance._knowledgeUIInitialized = true;
-
- this._Addon.EditorController.recordEditor(instance);
- }
-
- private addEditorInstanceListener() {
- Zotero.Notes.registerEditorInstance = new Proxy(
- Zotero.Notes.registerEditorInstance,
- {
- apply: (target, thisArg, argumentsList) => {
- target.apply(thisArg, argumentsList);
- this.onEditorInstanceCreated &&
- argumentsList.forEach(this.onEditorInstanceCreated.bind(this));
- },
- }
- );
- }
-
- private resetState(): void {
- // Reset preferrence state.
- this._Addon.TemplateController.resetTemplates();
- // Initialize citation style
- this._Addon.TemplateController.getCitationStyle();
- // Initialize sync notes
- this._Addon.SyncController.getSyncNoteIds();
- this._Addon.ZoteroViews.updateAutoInsertAnnotationsMenu();
- }
-
- public async onEditorEvent(message: EditorMessage) {
- this._Addon.toolkit.Tool.log(
- `Knowledge4Zotero: onEditorEvent\n${message.type}`
- );
- const mainNote = this._Addon.WorkspaceWindow.getWorkspaceNote();
- if (message.type === "openUserGuide") {
- /*
- message.content = {}
- */
- window.open(
- "chrome://Knowledge4Zotero/content/wizard.xul",
- "_blank",
- // margin=44+44/32+10+10+53, final space is 700*500
- "chrome,extrachrome,centerscreen,width=650,height=608"
- );
- } else if (message.type === "openAbout") {
- /*
- message.content = {}
- */
- // @ts-ignore
- window.openDialog(
- "chrome://Knowledge4Zotero/content/about.xul",
- "about",
- "chrome,centerscreen"
- );
- } else if (message.type === "openWorkspace") {
- /*
- message.content = {event?}
- */
- if (
- message.content.event &&
- (message.content.event as unknown as MouseEvent).shiftKey
- ) {
- await this._Addon.WorkspaceWindow.openWorkspaceWindow("window", true);
- } else {
- await this._Addon.WorkspaceWindow.openWorkspaceWindow();
- }
- } else if (message.type === "openWorkspaceInWindow") {
- /*
- message.content = {}
- */
- await this._Addon.WorkspaceWindow.openWorkspaceWindow("window", true);
- } else if (message.type === "closeWorkspace") {
- /*
- message.content = {}
- */
- this._Addon.WorkspaceWindow.closeWorkspaceWindow();
- } else if (message.type === "createWorkspace") {
- /*
- message.content = {}
- */
- const currentCollection = ZoteroPane.getSelectedCollection();
- if (!currentCollection) {
- this._Addon.ZoteroViews.showProgressWindow(
- "Better Notes",
- "Please select a collection before creating a new main note."
- );
- return;
- }
- const res = confirm(
- `${this._Addon.Locale.getString(
- "library.newMainNote.confirmHead"
- )} '${currentCollection.getName()}' ${this._Addon.Locale.getString(
- "library.newMainNote.confirmTail"
- )}`
- );
- if (!res) {
- return;
- }
- const header = prompt(
- "Enter new note header:",
- `New Note ${new Date().toLocaleString()}`
- );
- const noteID = await ZoteroPane.newNote();
- (Zotero.Items.get(noteID) as Zotero.Item).setNote(
- `
${header} \n
`
- );
- await this.onEditorEvent(
- new EditorMessage("setMainNote", {
- params: { itemID: noteID, enableConfirm: false, enableOpen: true },
- })
- );
- await this._Addon.WorkspaceWindow.openWorkspaceWindow();
- } else if (message.type === "selectMainNote") {
- /*
- message.content = {}
- */
- const ids = await this._Addon.ZoteroViews.openSelectItemsWindow();
- if (ids.length === 0) {
- this._Addon.ZoteroViews.showProgressWindow(
- "Knowledge",
- "No note selected."
- );
- return;
- } else if (ids.length > 1) {
- this._Addon.ZoteroViews.showProgressWindow(
- "Better Notes",
- "Please select a note item."
- );
- return;
- }
-
- const note = Zotero.Items.get(ids[0]) as Zotero.Item;
- if (note && note.isNote()) {
- this.onEditorEvent(
- new EditorMessage("setMainNote", {
- params: { itemID: note.id, enableConfirm: false, enableOpen: true },
- })
- );
- } else {
- this._Addon.ZoteroViews.showProgressWindow(
- "Better Notes",
- "Not a valid note item."
- );
- }
- } else if (message.type === "setMainNote") {
- /*
- message.content = {
- params: {itemID, enableConfirm, enableOpen}
- }
- */
- this._Addon.toolkit.Tool.log("setMainNote");
- let mainKnowledgeID = parseInt(
- Zotero.Prefs.get("Knowledge4Zotero.mainKnowledgeID") as string
- );
- let itemID = message.content.params.itemID;
- const item = Zotero.Items.get(itemID) as Zotero.Item;
- if (itemID === mainKnowledgeID) {
- this._Addon.ZoteroViews.showProgressWindow(
- "Better Notes",
- "Already a main Note."
- );
- return;
- } else if (!item.isNote()) {
- this._Addon.ZoteroViews.showProgressWindow(
- "Better Notes",
- "Not a valid note item."
- );
- } else {
- if (
- message.content.params.enableConfirm &&
- Zotero.Prefs.get("Knowledge4Zotero.mainKnowledgeID")
- ) {
- let confirmChange = confirm(
- "Will change current Knowledge Workspace. Confirm?"
- );
- if (!confirmChange) {
- return;
- }
- }
- if (message.content.params.enableOpen) {
- await this._Addon.WorkspaceWindow.openWorkspaceWindow();
- }
- await this._Addon.WorkspaceWindow.setWorkspaceNote("main", item);
- }
- } else if (message.type === "insertCitation") {
- /*
- message.content = {
- editorInstance?, event?, params?: {
- noteItem: noteItem,
- topItem: topItem,
- },
- }
- */
- let noteItem = message.content.editorInstance
- ? message.content.editorInstance._item
- : message.content.params.noteItem;
- let topItems: Zotero.Item[] = [];
- this._Addon.toolkit.Tool.log(message);
- if (message.content.event) {
- const topItemID = Number(
- message.content.event.target.id.split("-").pop()
- );
- topItems = Zotero.Items.get([topItemID]) as Zotero.Item[];
- }
- if (!topItems.length) {
- const ids = await this._Addon.ZoteroViews.openSelectItemsWindow();
-
- topItems = (Zotero.Items.get(ids) as Zotero.Item[]).filter(
- (item: Zotero.Item) => item.isRegularItem()
- );
- if (topItems.length === 0) {
- return;
- }
- }
- let format = this._Addon.TemplateController.getCitationStyle();
- const cite = Zotero.QuickCopy.getContentFromItems(
- topItems,
- format,
- null,
- 0
- );
- await this._Addon.NoteUtils.addLineToNote(noteItem, cite.html, -1);
- this._Addon.ZoteroViews.showProgressWindow(
- "Better Notes",
- `${
- topItems.length > 3
- ? topItems.length + "items"
- : topItems.map((i) => i.getField("title")).join("\n")
- } cited.`
- );
- } else if (message.type === "setRecentMainNote") {
- /*
- message.content = {
- editorInstance?, event?
- }
- */
- await this._Addon.WorkspaceWindow.setWorkspaceNote(
- "main",
- Zotero.Items.get(
- message.content.event.target.id.split("-").pop()
- ) as Zotero.Item
- );
- } else if (message.type === "addToNoteEnd") {
- /*
- message.content = {
- editorInstance
- }
- */
- this._Addon.toolkit.Tool.log("addToNoteEnd");
- await this._Addon.NoteUtils.addLinkToNote(
- mainNote,
- (message.content.editorInstance as Zotero.EditorInstance)._item,
- // -1 for automatically insert to current selected line or end of note
- -1,
- ""
- );
- } else if (message.type === "addToNote") {
- /*
- message.content = {
- editorInstance?,
- event?,
- params: {
- itemID: number,
- lineIndex: number,
- sectionName: string
- }
- }
- */
- this._Addon.toolkit.Tool.log("addToNote");
- let lineIndex = message.content.params?.lineIndex;
- if (typeof lineIndex === "undefined") {
- const eventInfo = (message.content.event as XUL.XULEvent).target.id;
- this._Addon.toolkit.Tool.log(eventInfo);
- const idSplit = eventInfo.split("-");
- lineIndex = parseInt(idSplit.pop());
- }
- let sectionName = message.content.params?.sectionName;
- if (typeof sectionName === "undefined") {
- sectionName = (message.content.event as XUL.XULEvent).target.innerText;
- }
-
- await this._Addon.NoteUtils.addLinkToNote(
- mainNote,
- message.content.params?.itemID
- ? (Zotero.Items.get(message.content.params.itemID) as Zotero.Item)
- : (message.content.editorInstance as Zotero.EditorInstance)._item,
- lineIndex,
- sectionName
- );
- } else if (message.type === "jumpNode") {
- /*
- message.content = {
- params: {id, lineIndex}
- }
- */
- this._Addon.toolkit.Tool.log(message.content.params);
- let editorInstance =
- await this._Addon.WorkspaceWindow.getWorkspaceEditorInstance();
- // Set node id
- this._Addon.WorkspaceOutline.currentNodeID = parseInt(
- message.content.params.id
- );
- this._Addon.EditorViews.scrollToLine(
- editorInstance,
- // Scroll to line
- message.content.params.lineIndex
- );
- } else if (message.type === "closePreview") {
- /*
- message.content = {
- editorInstance
- }
- */
- const _window =
- this._Addon.WorkspaceWindow.getWorkspaceWindow() as Window;
- const splitter = _window.document.getElementById("preview-splitter");
- splitter && splitter.setAttribute("state", "collapsed");
- } else if (message.type === "onNoteLink") {
- /*
- message.content = {
- params: {
- item: Zotero.Item | boolean,
- forceStandalone: boolean,
- infoText: string
- args: {}
- }
- }
- */
- const noteItem = message.content.params.item;
- const forceStandalone = message.content.params.forceStandalone;
- let _window = this._Addon.WorkspaceWindow.getWorkspaceWindow();
- if (!noteItem) {
- this._Addon.toolkit.Tool.log(
- `Knowledge4Zotero: ${message.content.params.infoText}`
- );
- }
- this._Addon.toolkit.Tool.log(
- `Knowledge4Zotero: onNoteLink ${noteItem.id}`
- );
- if (
- !forceStandalone &&
- _window &&
- (noteItem.id === this._Addon.WorkspaceWindow.getWorkspaceNote().id ||
- noteItem.id === this._Addon.WorkspaceWindow.previewItemID)
- ) {
- // Scroll to line directly
- await this._Addon.WorkspaceWindow.openWorkspaceWindow();
- } else {
- this._Addon.EditorController.startWaiting();
- if (_window && !forceStandalone) {
- await this._Addon.WorkspaceWindow.setWorkspaceNote(
- "preview",
- noteItem
- );
- await this._Addon.WorkspaceWindow.openWorkspaceWindow();
- } else {
- ZoteroPane.openNoteWindow(noteItem.id);
- }
- await this._Addon.EditorController.waitForEditor();
- }
-
- if (message.content.params.args.line) {
- Zotero.Notes._editorInstances
- .filter((e) => e._item.id === noteItem.id)
- .forEach((e) => {
- this._Addon.EditorViews.scrollToLine(
- e,
- // Scroll to line
- message.content.params.args.line
- );
- });
- }
- } else if (message.type === "updateAutoAnnotation") {
- /*
- message.content = {
- editorInstance
- }
- */
- let autoAnnotation = Zotero.Prefs.get("Knowledge4Zotero.autoAnnotation");
- autoAnnotation = !autoAnnotation;
- Zotero.Prefs.set("Knowledge4Zotero.autoAnnotation", autoAnnotation);
- this._Addon.ZoteroViews.updateAutoInsertAnnotationsMenu();
- } else if (message.type === "insertNotes") {
- /*
- message.content = {}
- */
- const ids = await this._Addon.ZoteroViews.openSelectItemsWindow();
- const notes = (Zotero.Items.get(ids) as Zotero.Item[]).filter(
- (item: Zotero.Item) => item.isNote()
- );
- if (notes.length === 0) {
- return;
- }
-
- const newLines: string[] = [];
- newLines.push("Imported Notes ");
- newLines.push("
");
-
- for (const note of notes) {
- const linkURL = this._Addon.NoteUtils.getNoteLink(note);
- const linkText = note.getNoteTitle().trim();
- newLines.push(
- `${linkText ? linkText : linkURL}
`
- );
- newLines.push("
");
- }
- await this._Addon.NoteUtils.addLineToNote(
- mainNote,
- newLines.join("\n"),
- -1
- );
- } else if (message.type === "insertTextUsingTemplate") {
- /*
- message.content = {
- params: {templateName, targetItemId, useMainNote}
- }
- */
- const newLines: string[] = [];
- const targetItem = Zotero.Items.get(
- message.content.params.targetItemId
- ) as Zotero.Item;
-
- const progressWindow = this._Addon.ZoteroViews.showProgressWindow(
- "Running Template",
- message.content.params.templateName,
- "default",
- -1
- );
- const renderredTemplate =
- (await this._Addon.TemplateController.renderTemplateAsync(
- message.content.params.templateName
- )) as string;
-
- if (renderredTemplate) {
- newLines.push(renderredTemplate);
- newLines.push("
");
- const html = newLines.join("\n");
- if (!targetItem) {
- this._Addon.toolkit.Tool.log(html);
- this._Addon.toolkit.Tool.getCopyHelper()
- .addText(html, "text/html")
- .addText(
- await this._Addon.NoteParse.parseHTMLToMD(html),
- "text/unicode"
- )
- .copy();
- progressWindow.changeHeadline("Template Copied");
- } else {
- // End of line
- await this._Addon.NoteUtils.addLineToNote(
- targetItem,
- html,
- -1,
- false
- );
- progressWindow.changeHeadline("Running Template Finished");
- }
- } else {
- progressWindow.changeHeadline("Running Template Failed");
- }
- progressWindow.startCloseTimer(5000);
- } else if (message.type === "insertItemUsingTemplate") {
- /*
- message.content = {
- params: {templateName, targetItemId, useMainNote}
- }
- */
- const targetItem = Zotero.Items.get(
- message.content.params.targetItemId
- ) as Zotero.Item;
-
- const ids = await this._Addon.ZoteroViews.openSelectItemsWindow();
- const items = Zotero.Items.get(ids) as Zotero.Item[];
- if (items.length === 0) {
- return;
- }
- const progressWindow = this._Addon.ZoteroViews.showProgressWindow(
- "Running Template",
- message.content.params.templateName,
- "default",
- -1
- );
- const newLines: string[] = [];
- newLines.push("
");
-
- const toCopyImage: Zotero.Item[] = [];
-
- const copyNoteImage = (noteItem: Zotero.Item) => {
- toCopyImage.push(noteItem);
- };
-
- const editor = message.content.params.useMainNote
- ? await this._Addon.WorkspaceWindow.getWorkspaceEditorInstance(
- "main",
- false
- )
- : this._Addon.EditorController.activeEditor;
-
- const sharedObj = {};
-
- let renderredTemplate =
- await this._Addon.TemplateController.renderTemplateAsync(
- message.content.params.templateName,
- "items, copyNoteImage, editor, sharedObj",
- [items, copyNoteImage, editor, sharedObj],
- true,
- "beforeloop"
- );
-
- if (renderredTemplate) {
- newLines.push(renderredTemplate);
- newLines.push("
");
- }
-
- for (const topItem of items) {
- /*
- Available variables:
- topItem, itemNotes, copyNoteImage, editor
- */
-
- const itemNotes: Zotero.Item[] = topItem.isNote()
- ? []
- : (Zotero.Items.get(topItem.getNotes()) as Zotero.Item[]);
-
- renderredTemplate =
- await this._Addon.TemplateController.renderTemplateAsync(
- message.content.params.templateName,
- "topItem, itemNotes, copyNoteImage, editor, sharedObj",
- [topItem, itemNotes, copyNoteImage, editor, sharedObj],
- true,
- "default"
- );
-
- if (renderredTemplate) {
- newLines.push(renderredTemplate);
- newLines.push("
");
- }
- }
-
- renderredTemplate =
- await this._Addon.TemplateController.renderTemplateAsync(
- message.content.params.templateName,
- "items, copyNoteImage, editor, sharedObj",
- [items, copyNoteImage, editor, sharedObj],
- true,
- "afterloop"
- );
-
- if (renderredTemplate) {
- newLines.push(renderredTemplate);
- newLines.push("
");
- }
-
- if (newLines) {
- const html = newLines.join("\n");
- if (!targetItem) {
- this._Addon.toolkit.Tool.log(html);
-
- this._Addon.toolkit.Tool.getCopyHelper()
- .addText(html, "text/html")
- .addText(
- await this._Addon.NoteParse.parseHTMLToMD(html),
- "text/unicode"
- )
- .copy();
- progressWindow.changeHeadline("Template Copied");
- } else {
- const forceMetadata = toCopyImage.length > 0;
- this._Addon.toolkit.Tool.log(toCopyImage);
- await this._Addon.NoteUtils.addLineToNote(
- targetItem,
- html,
- -1,
- forceMetadata
- );
- await Zotero.DB.executeTransaction(async () => {
- for (const subNote of toCopyImage) {
- await Zotero.Notes.copyEmbeddedImages(subNote, targetItem);
- }
- });
- progressWindow.changeHeadline("Running Template Finished");
- }
- } else {
- progressWindow.changeHeadline("Running Template Failed");
- }
- progressWindow.startCloseTimer(5000);
- } else if (message.type === "editTemplate") {
- /*
- message.content = {}
- */
- this._Addon.TemplateWindow.openEditor();
- } else if (message.type === "export") {
- /*
- message.content = {
- editorInstance?, params?: {item}
- }
- */
- let item = message.content.editorInstance
- ? message.content.editorInstance._item
- : message.content.params.item;
-
- if (!item) {
- item = this._Addon.WorkspaceWindow.getWorkspaceNote();
- }
- // If this note is in sync list, open sync window
- if (this._Addon.SyncController.getSyncNoteIds().includes(item.id)) {
- const io = {
- dataIn: item,
- dataOut: {} as any,
- deferred: Zotero.Promise.defer(),
- };
-
- (window as unknown as XUL.XULWindow).openDialog(
- "chrome://Knowledge4Zotero/content/sync.xul",
- "",
- "chrome,centerscreen,width=500,height=200",
- io
- );
- await io.deferred.promise;
- if (!io.dataOut.export) {
- return;
- }
- }
- const io = {
- dataIn: null,
- dataOut: null,
- deferred: Zotero.Promise.defer(),
- };
-
- (window as unknown as XUL.XULWindow).openDialog(
- "chrome://Knowledge4Zotero/content/export.xul",
- "",
- "chrome,centerscreen,width=400,height=500,resizable=yes",
- io
- );
- await io.deferred.promise;
-
- const options = io.dataOut as any;
- if (!options) {
- return;
- }
- if (options.exportMD && options.exportSubMD) {
- await this._Addon.NoteExport.exportNotesToMDFiles([item], {
- useEmbed: false,
- useSync: options.exportAutoSync,
- withMeta: options.exportYAMLHeader,
- });
- } else {
- await this._Addon.NoteExport.exportNote(item, options);
- }
- } else if (message.type === "exportNotes") {
- /*
- message.content = {}
- */
- const items = ZoteroPane.getSelectedItems();
- const noteItems: Zotero.Item[] = [];
- items.forEach((item) => {
- if (item.isNote()) {
- noteItems.push(item);
- }
- if (item.isRegularItem()) {
- noteItems.splice(
- 0,
- 0,
- ...(Zotero.Items.get(item.getNotes()) as Zotero.Item[])
- );
- }
- });
- if (noteItems.length === 0) {
- this._Addon.ZoteroViews.showProgressWindow(
- "Better Notes",
- "No standalone/item note selected."
- );
- } else if (noteItems.length === 1) {
- this.onEditorEvent(
- new EditorMessage("export", { params: { item: noteItems[0] } })
- );
- } else {
- const useSingleFile = confirm(
- this._Addon.Locale.getString("export.withLinkedNotes.confirm")
- );
- const withMeta = confirm(
- this._Addon.Locale.getString("export.withYAML.confirm")
- );
- await this._Addon.NoteExport.exportNotesToMDFiles(noteItems, {
- useEmbed: !useSingleFile,
- withMeta: withMeta,
- });
- }
- } else if (message.type === "sync") {
- /*
- message.content = {
- editorInstance
- }
- */
- const note = this._Addon.WorkspaceWindow.getWorkspaceNote();
- if (this._Addon.SyncController.isSyncNote(note)) {
- this._Addon.SyncController.doSync([note]);
- } else {
- await this._Addon.NoteExport.exportNotesToMDFiles([note], {
- useEmbed: false,
- useSync: true,
- withMeta: true,
- });
- }
- } else if (message.type === "openAttachment") {
- /*
- message.content = {
- editorInstance
- }
- */
- const editor = message.content.editorInstance as Zotero.EditorInstance;
- const note = editor._item;
- if (note.parentItem) {
- const attachment = await note.parentItem.getBestAttachment();
- this._Addon.toolkit.Tool.log(attachment);
- if (!attachment) {
- return;
- }
- try {
- this._Addon.toolkit.Tool.log("Launching PDF without page number");
- let zp = Zotero.getActiveZoteroPane();
- if (zp) {
- zp.viewAttachment([attachment.id]);
- }
- Zotero.Notifier.trigger("open", "file", attachment.id);
- } catch (e) {
- this._Addon.toolkit.Tool.log("Open attachment failed:");
- this._Addon.toolkit.Tool.log(attachment);
- this._Addon.ZoteroViews.showProgressWindow(
- "Better Notes",
- "Error occurred on opening attachemnts.",
- "fail"
- );
- }
- }
- } else if (message.type === "setOCREngine") {
- /*
- message.content = {
- params: { engine: string }
- }
- */
- const engine = message.content.params.engine;
- if (engine === "mathpix") {
- Zotero.Prefs.set(
- "Knowledge4Zotero.OCRMathpix.Appid",
- prompt(
- "Please input app_id",
- Zotero.Prefs.get("Knowledge4Zotero.OCRMathpix.Appid") as string
- )
- );
- Zotero.Prefs.set(
- "Knowledge4Zotero.OCRMathpix.Appkey",
- prompt(
- "Please input app_key",
- Zotero.Prefs.get("Knowledge4Zotero.OCRMathpix.Appkey") as string
- )
- );
- } else if (engine === "xunfei") {
- Zotero.Prefs.set(
- "Knowledge4Zotero.OCRXunfei.APPID",
- prompt(
- "Please input APPID",
- Zotero.Prefs.get("Knowledge4Zotero.OCRXunfei.APPID") as string
- )
- );
- Zotero.Prefs.set(
- "Knowledge4Zotero.OCRXunfei.APISecret",
- prompt(
- "Please input APISecret",
- Zotero.Prefs.get("Knowledge4Zotero.OCRXunfei.APISecret") as string
- )
- );
- Zotero.Prefs.set(
- "Knowledge4Zotero.OCRXunfei.APIKey",
- prompt(
- "Please input APIKey",
- Zotero.Prefs.get("Knowledge4Zotero.OCRXunfei.APIKey") as string
- )
- );
- }
- Zotero.Prefs.set("Knowledge4Zotero.OCREngine", engine);
- } else if (message.type == "convertMD") {
- /*
- message.content = {}
- */
- const source = Zotero.Utilities.Internal.getClipboard("text/unicode");
- if (!source) {
- this._Addon.ZoteroViews.showProgressWindow(
- "Better Notes",
- "No MarkDown found."
- );
- return;
- }
- const html = await this._Addon.NoteParse.parseMDToHTML(source);
- this._Addon.toolkit.Tool.log(source, html);
- this._Addon.toolkit.Tool.getCopyHelper()
- .addText(html, "text/html")
- .copy();
-
- this._Addon.ZoteroViews.showProgressWindow(
- "Better Notes",
- "Converted MarkDown is updated to the clipboard. You can paste them in the note."
- );
- } else if (message.type == "convertAsciiDoc") {
- /*
- message.content = {}
- */
- const source = Zotero.Utilities.Internal.getClipboard("text/unicode");
- if (!source) {
- this._Addon.ZoteroViews.showProgressWindow(
- "Better Notes",
- "No AsciiDoc found."
- );
- return;
- }
- const html = this._Addon.NoteParse.parseAsciiDocToHTML(source);
- this._Addon.toolkit.Tool.log(source, html);
- this._Addon.toolkit.Tool.getCopyHelper()
- .addText(html, "text/html")
- .copy();
-
- this._Addon.ZoteroViews.showProgressWindow(
- "Better Notes",
- "Converted AsciiDoc is updated to the clipboard. You can paste them in the note."
- );
- } else {
- this._Addon.toolkit.Tool.log("message not handled.");
- }
- }
-}
-
-export default ZoteroEvents;
diff --git a/src/zotero/locale.ts b/src/zotero/locale.ts
deleted file mode 100644
index 21b39d1..0000000
--- a/src/zotero/locale.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import AddonModule from "../module";
-import { addonRef } from "../../package.json";
-
-class AddonLocale extends AddonModule {
- private stringBundle: any;
-
- public initLocale() {
- this.stringBundle = Components.classes["@mozilla.org/intl/stringbundle;1"]
- .getService(Components.interfaces.nsIStringBundleService)
- .createBundle(`chrome://${addonRef}/locale/addon.properties`);
- }
-
- public getString(localString: string): string {
- return this.stringBundle.GetStringFromName(localString);
- }
-}
-
-export default AddonLocale;
diff --git a/src/zotero/notifies.ts b/src/zotero/notifies.ts
deleted file mode 100644
index c1e3b1d..0000000
--- a/src/zotero/notifies.ts
+++ /dev/null
@@ -1,291 +0,0 @@
-import TreeModel = require("tree-model");
-import BetterNotes from "../addon";
-import AddonBase from "../module";
-import { EditorMessage } from "../utils";
-
-export default class ZoteroNotifies extends AddonBase {
- notifierCbkDict: { [key: string]: Function };
- constructor(parent: BetterNotes) {
- super(parent);
- this.notifierCbkDict = {};
- }
-
- public registerNotifyListener(name: string, cbk: Function) {
- this.notifierCbkDict[name] = cbk;
- }
-
- public unregisterNotifyListener(name: string) {
- delete this.notifierCbkDict[name];
- }
-
- initNotifyCallback() {
- // Register the callback in Zotero as an item observer
- const notifierCallback = {
- notify: async (
- event: string,
- type: string,
- ids: Array,
- extraData: object
- ) => {
- for (const cbk of Object.values(this.notifierCbkDict)) {
- try {
- (cbk as Function)(event, type, ids, extraData);
- } catch (e) {
- this._Addon.toolkit.Tool.log(e);
- }
- }
- },
- };
- let notifierID = Zotero.Notifier.registerObserver(notifierCallback, [
- "item",
- "tab",
- "file",
- "item-tag",
- ]);
-
- this.registerDefaultCallbacks();
-
- // Unregister callback when the window closes (important to avoid a memory leak)
- window.addEventListener(
- "unload",
- (e) => {
- Zotero.Notifier.unregisterObserver(notifierID);
- },
- false
- );
- }
-
- // TODO: move these to seperate functions under different modules
- registerDefaultCallbacks() {
- const itemModifyCallback = async (
- event: string,
- type: string,
- ids: Array,
- extraData: object
- ) => {
- if (event === "modify" && type === "item") {
- if (
- ids.indexOf(
- Zotero.Prefs.get("Knowledge4Zotero.mainKnowledgeID") as number
- ) >= 0
- ) {
- this._Addon.toolkit.Tool.log("main knowledge modify check.");
- this._Addon.WorkspaceOutline.updateOutline();
- this._Addon.ZoteroViews.updateWordCount();
- }
- // Check Note Sync
- const syncIds = this._Addon.SyncController.getSyncNoteIds();
- const modifiedSyncIds = ids.filter((id) =>
- syncIds.includes(id as number)
- ) as number[];
- if (modifiedSyncIds.length > 0) {
- // Delay so that item content is ready
- setTimeout(() => {
- this._Addon.SyncController.doSync(
- Zotero.Items.get(modifiedSyncIds)
- );
- }, 10000);
- this._Addon.toolkit.Tool.log("sync planned.");
- }
- }
- };
-
- const annotationDispalyCallback = async (
- event: string,
- type: string,
- ids: Array,
- extraData: object
- ) => {
- if (
- (event == "select" &&
- type == "tab" &&
- extraData[ids[0]].type == "reader") ||
- (event === "add" &&
- type === "item" &&
- (Zotero.Items.get(ids as number[]) as Zotero.Item[]).filter(
- (item) => {
- return item.isAnnotation();
- }
- ).length > 0) ||
- (event === "close" && type === "tab") ||
- (event === "open" && type === "file")
- ) {
- await this._Addon.ReaderViews.buildReaderAnnotationButtons();
- }
- };
-
- const addWorkspaceTabCallback = async (
- event: string,
- type: string,
- ids: Array,
- extraData: object
- ) => {
- if (
- event == "add" &&
- type == "tab" &&
- ids[0] === this._Addon.WorkspaceWindow.workspaceTabId
- ) {
- const tabItem = document.querySelector(`.tab[data-id=${ids[0]}]`);
- const tabTitle = tabItem && tabItem.querySelector(".tab-name");
- tabTitle &&
- (tabTitle.innerHTML = `${this._Addon.ZoteroViews.icons["tabIcon"]}${tabTitle.innerHTML}`);
- }
- };
-
- const selectWorkspaceTabCallback = async (
- event: string,
- type: string,
- ids: Array,
- extraData: object
- ) => {
- if (
- event == "select" &&
- type == "tab" &&
- extraData[ids[0]].type == "betternotes"
- ) {
- let t = 0;
- await this._Addon.WorkspaceWindow.waitWorkspaceReady();
- while (
- !(await this._Addon.WorkspaceWindow.getWorkspaceEditorInstance(
- "main",
- false
- )) &&
- t < 100
- ) {
- t += 1;
- this._Addon.WorkspaceWindow.setWorkspaceNote(
- "main",
- undefined,
- false
- );
- await Zotero.Promise.delay(100);
- }
-
- const _tabCover = document.getElementById("zotero-tab-cover");
- const _contextPane = document.getElementById(
- "zotero-context-pane"
- ) as XUL.Element;
- const _contextPaneSplitter = document.getElementById(
- "zotero-context-splitter"
- ) as XUL.Element;
- const _tabToolbar = document.getElementById("zotero-tab-toolbar");
- _contextPaneSplitter.setAttribute("hidden", true);
- _contextPane.setAttribute("collapsed", true);
- _tabToolbar && (_tabToolbar.hidden = true);
- _tabCover && (_tabCover.hidden = true);
- this._Addon.ZoteroViews.switchRealMenuBar(false);
- this._Addon.ZoteroViews.switchKey(false);
- this._Addon.ZoteroViews.updateWordCount();
- } else {
- this._Addon.ZoteroViews.switchRealMenuBar(true);
- this._Addon.ZoteroViews.switchKey(true);
- }
- };
-
- const autoAnnotationCallback = async (
- event: string,
- type: string,
- ids: Array,
- extraData: object
- ) => {
- if (
- Zotero.Prefs.get("Knowledge4Zotero.autoAnnotation") &&
- event === "add" &&
- type === "item" &&
- (Zotero.Items.get(ids as number[]) as Zotero.Item[]).filter((item) => {
- return item.isAnnotation();
- }).length > 0
- ) {
- this._Addon.toolkit.Tool.log("autoAnnotation");
- const annotations = (
- Zotero.Items.get(ids as number[]) as Zotero.Item[]
- ).filter((item) => {
- return item.isAnnotation();
- });
- this._Addon.NoteUtils.addAnnotationsToNote(
- this._Addon.WorkspaceWindow.getWorkspaceNote(),
- annotations,
- -1,
- true
- );
- }
- };
-
- const addToNoteTriggeredByTagCallback = async (
- event: string,
- type: string,
- ids: Array,
- extraData: object
- ) => {
- if (event === "add" && type === "item-tag") {
- const nodes: TreeModel.Node[] =
- this._Addon.NoteUtils.getNoteTreeAsList(
- this._Addon.WorkspaceWindow.getWorkspaceNote()
- );
- const headings: string[] = nodes.map((node) => node.model.name);
- this._Addon.toolkit.Tool.log(ids, extraData, headings);
-
- for (const tagId of ids.filter((t) => extraData[t].tag[0] === "#")) {
- const tagName = (extraData[tagId].tag as string).slice(1).trim();
- if (headings.includes(tagName) || tagName === "#") {
- let lineIndex: number;
- let sectionName: string;
- if (tagName === "#") {
- lineIndex = -1;
- sectionName = "";
- } else {
- const targetNode = nodes.find(
- (node) => node.model.name === tagName
- );
- lineIndex = targetNode.model.endIndex;
- sectionName = targetNode.model.name;
- }
-
- const item = Zotero.Items.get(
- (tagId as string).split("-")[0]
- ) as Zotero.Item;
- if (item.isAnnotation()) {
- this._Addon.NoteUtils.addAnnotationsToNote(
- this._Addon.WorkspaceWindow.getWorkspaceNote(),
- [item],
- -1,
- true
- );
- } else if (item.isNote()) {
- this._Addon.ZoteroEvents.onEditorEvent(
- new EditorMessage("addToNote", {
- params: {
- itemID: item.id,
- lineIndex: lineIndex,
- sectionName: sectionName,
- },
- })
- );
- }
- }
- }
- }
- };
- this.registerNotifyListener("itemModifyCallback", itemModifyCallback);
- this.registerNotifyListener(
- "annotationDispalyCallback",
- annotationDispalyCallback
- );
- this.registerNotifyListener(
- "addWorkspaceTabCallback",
- addWorkspaceTabCallback
- );
- this.registerNotifyListener(
- "selectWorkspaceTabCallback",
- selectWorkspaceTabCallback
- );
- this.registerNotifyListener(
- "autoAnnotationCallback",
- autoAnnotationCallback
- );
- this.registerNotifyListener(
- "addToNoteTriggeredByTagCallback",
- addToNoteTriggeredByTagCallback
- );
- }
-}
diff --git a/src/zotero/views.ts b/src/zotero/views.ts
deleted file mode 100644
index 3309085..0000000
--- a/src/zotero/views.ts
+++ /dev/null
@@ -1,426 +0,0 @@
-/*
- * This file contains the Zotero UI code.
- */
-
-import BetterNotes from "../addon";
-import { EditorMessage } from "../utils";
-import AddonBase from "../module";
-
-class ZoteroViews extends AddonBase {
- progressWindowIcon: object;
- icons: object;
-
- constructor(parent: BetterNotes) {
- super(parent);
- this.progressWindowIcon = {
- success: "chrome://zotero/skin/tick.png",
- fail: "chrome://zotero/skin/cross.png",
- default: "chrome://Knowledge4Zotero/skin/favicon.png",
- };
- this.icons = {
- tabIcon: ` `,
- openWorkspaceCollectionView: ` `,
- };
- }
-
- public hideMenuBar(_document: Document) {
- _document.getElementById("better-notes-menu").hidden = true;
- }
-
- public keppDefaultMenuOrder() {
- const fileMenu = document.querySelector("#menu_FilePopup");
- const editMenu = document.querySelector("#menu_EditPopup");
- const exit = fileMenu.querySelector("#menu_FileQuitItem");
- // exit.remove();
- const prefs = editMenu.querySelector("#menu_preferences");
- // prefs.remove();
- if (exit) {
- for (const ele of fileMenu.querySelectorAll(".menu-betternotes")) {
- exit.before(ele);
- }
- }
- if (prefs) {
- for (const ele of editMenu.querySelectorAll(".menu-betternotes")) {
- prefs.before(ele);
- }
- }
- }
-
- public switchRealMenuBar(hidden: boolean) {
- // We only handle hide. The show will be handled by the ZoteroStandalone.switchMenuType
- document
- .querySelectorAll(".menu-type-betternotes")
- .forEach((el) => ((el as HTMLElement).hidden = hidden));
-
- // Disable Zotero pdf export
- (document.getElementById("menu_export_files") as XUL.MenuItem).disabled =
- !hidden;
- }
-
- public switchKey(disabled: boolean) {
- document
- .querySelectorAll(".key-type-betternotes")
- .forEach((el) => (el as XUL.Element).setAttribute("disabled", disabled));
- }
-
- public addNewMainNoteButton() {
- const menupopup = document
- .querySelector("#zotero-tb-note-add")
- .querySelector("menupopup") as XUL.MenuPopup;
- this._Addon.toolkit.UI.insertMenuItem(menupopup, {
- tag: "menuitem",
- label: this._Addon.Locale.getString("library.newMainNote"),
- icon: "chrome://Knowledge4Zotero/skin/favicon.png",
- commandListener: (e) => {
- this._Addon.ZoteroEvents.onEditorEvent(
- new EditorMessage("createWorkspace", {})
- );
- },
- });
- this._Addon.toolkit.UI.insertMenuItem(menupopup, {
- tag: "menuitem",
- label: this._Addon.Locale.getString("library.importMD"),
- icon: "chrome://Knowledge4Zotero/skin/favicon.png",
- commandListener: async (e) => {
- await this._Addon.NoteImport.doImport();
- },
- });
- }
-
- public addOpenWorkspaceButton() {
- // Left collection tree view button
- const treeRow = this._Addon.toolkit.UI.creatElementsFromJSON(document, {
- tag: "html:div",
- attributes: {
- class: "row",
- style: "max-height: 22px; margin: 0 0 0 0; padding: 0 6px 0 6px;",
- },
- listeners: [
- {
- type: "click",
- listener: async (e: MouseEvent) => {
- if (e.shiftKey) {
- await this._Addon.WorkspaceWindow.openWorkspaceWindow(
- "window",
- true
- );
- } else {
- await this._Addon.WorkspaceWindow.openWorkspaceWindow();
- }
- },
- },
- {
- type: "mouseover",
- listener: (e) => {
- treeRow.setAttribute(
- "style",
- "max-height: 22px; margin: 0 0 0 0; padding: 0 6px 0 6px; background-color: grey;"
- );
- },
- },
- {
- type: "mouseleave",
- listener: (e) => {
- treeRow.setAttribute(
- "style",
- "max-height: 22px; margin: 0 0 0 0; padding: 0 6px 0 6px;"
- );
- },
- },
- {
- type: "mousedown",
- listener: (e) => {
- treeRow.setAttribute(
- "style",
- "max-height: 22px; margin: 0 0 0 0; padding: 0 6px 0 6px; color: #FFFFFF;"
- );
- },
- },
- {
- type: "mouseup",
- listener: (e) => {
- treeRow.setAttribute(
- "style",
- "max-height: 22px; margin: 0 0 0 0; padding: 0 6px 0 6px;"
- );
- },
- },
- ],
- subElementOptions: [
- {
- tag: "div",
- attributes: {
- class: "icon icon-twisty twisty open",
- },
- directAttributes: {
- innerHTML: this.icons["openWorkspaceCollectionView"],
- },
- },
- {
- tag: "div",
- attributes: {
- class: "icon icon-bg cell-icon",
- style:
- "background-image:url(chrome://Knowledge4Zotero/skin/favicon.png)",
- },
- },
- {
- tag: "div",
- attributes: {
- class: "cell-text",
- style: "margin-left: 6px;",
- },
- directAttributes: {
- innerHTML: this._Addon.Locale.getString("library.openWorkspace"),
- },
- },
- ],
- }) as HTMLDivElement;
- document
- .getElementById("zotero-collections-tree-container")
- .children[0].before(treeRow);
- }
-
- public updateTemplateMenu(
- event: Event,
- type: "Item" | "Text",
- useMainNote: boolean = true
- ) {
- // @ts-ignore
- const menupopup = event.originalTarget as XUL.MenuPopup;
- const _document = menupopup.ownerDocument;
-
- // If no note is activated, use copy
- const targetItemId =
- this._Addon.EditorController.activeEditor &&
- Zotero.Notes._editorInstances.includes(
- this._Addon.EditorController.activeEditor
- )
- ? this._Addon.EditorController.activeEditor._item.id
- : Zotero_Tabs.selectedID === this._Addon.WorkspaceWindow.workspaceTabId
- ? this._Addon.WorkspaceWindow.getWorkspaceNote().id
- : -1;
- this._Addon.toolkit.Tool.log("updateTemplateMenu");
- let templates = this._Addon.TemplateController.getTemplateKeys()
- .filter((e) => e.name.indexOf(type) !== -1)
- .filter(
- (e) =>
- !this._Addon.TemplateController._systemTemplateNames.includes(e.name)
- );
- menupopup.innerHTML = "";
- if (templates.length === 0) {
- templates = [
- {
- name: "No Template",
- text: "",
- disabled: true,
- },
- ];
- }
- for (const template of templates) {
- const menuitem = this._Addon.toolkit.UI.creatElementsFromJSON(_document, {
- tag: "menuitem",
- namespace: "xul",
- attributes: {
- label: template.name,
- disabled: template.disabled,
- },
- listeners: [
- {
- type: "command",
- listener: (e) => {
- this._Addon.ZoteroEvents.onEditorEvent({
- type: `insert${type}UsingTemplate`,
- content: {
- params: {
- templateName: template.name,
- targetItemId,
- useMainNote,
- },
- },
- });
- },
- },
- ],
- }) as XUL.MenuItem;
- menupopup.append(menuitem);
- }
- }
-
- // To deprecate
- public updateCitationStyleMenu() {
- const _window = this._Addon.WorkspaceMenu.getWorkspaceMenuWindow();
- this._Addon.toolkit.Tool.log(`updateCitationStyleMenu`);
-
- const popup = _window.document.getElementById("menu_citeSettingPopup");
- popup.innerHTML = "";
-
- let format = this._Addon.TemplateController.getCitationStyle();
-
- // add styles to list
- const styles = Zotero.Styles.getVisible();
- styles.forEach((style) => {
- const val = JSON.stringify({
- mode: "bibliography",
- contentType: "html",
- id: style.styleID,
- locale: "",
- });
- const itemNode = this._Addon.toolkit.UI.createElement(
- _window.document,
- "menuitem",
- "xul"
- ) as XUL.MenuItem;
- itemNode.setAttribute("value", val);
- itemNode.setAttribute("label", style.title);
- itemNode.setAttribute("type", "checkbox");
- itemNode.setAttribute(
- "oncommand",
- "Zotero.Prefs.set('Knowledge4Zotero.citeFormat', event.target.value)"
- );
- popup.appendChild(itemNode);
-
- if (format.id == style.styleID) {
- itemNode.setAttribute("checked", true);
- }
- });
- }
-
- public updateOCRStyleMenu() {
- this._Addon.toolkit.Tool.log(`updateOCRStyleMenu`);
- const popup = document.getElementById("menu_ocrsettingpopup");
- Array.prototype.forEach.call(popup.children, (e) =>
- e.setAttribute("checked", false)
- );
- let engine = Zotero.Prefs.get("Knowledge4Zotero.OCREngine");
- if (!engine) {
- engine = "bing";
- Zotero.Prefs.set("Knowledge4Zotero.OCREngine", engine);
- }
- (
- document.getElementById(`menu_ocr_${engine}_betternotes`) as XUL.MenuItem
- ).setAttribute("checked", true);
- }
-
- public updateWordCount() {
- const _window = this._Addon.WorkspaceMenu.getWorkspaceMenuWindow();
- if (!_window) {
- return;
- }
- this._Addon.toolkit.Tool.log("updateWordCount");
-
- const menuitem = _window.document.getElementById(
- "menu_wordcount_betternotes"
- );
- function fnGetCpmisWords(str) {
- let sLen = 0;
- try {
- // replace break lines
- str = str.replace(/(\r\n+|\s+| +)/g, "龘");
- // letter, numbers to 'm' letter
- str = str.replace(/[\x00-\xff]/g, "m");
- // make neighbor 'm' to be one letter
- str = str.replace(/m+/g, "*");
- // remove white space
- str = str.replace(/龘+/g, "");
- sLen = str.length;
- } catch (e) {}
- return sLen;
- }
- menuitem.setAttribute(
- "label",
- `Word Count: ${fnGetCpmisWords(
- this._Addon.NoteParse.parseNoteHTML(
- this._Addon.WorkspaceWindow.getWorkspaceNote()
- ).innerText
- )}`
- );
- }
-
- public updateAutoInsertAnnotationsMenu() {
- const _window = this._Addon.WorkspaceMenu.getWorkspaceMenuWindow();
-
- this._Addon.toolkit.Tool.log("updateAutoInsertAnnotationsMenu");
-
- let autoAnnotation = Zotero.Prefs.get("Knowledge4Zotero.autoAnnotation");
- if (typeof autoAnnotation === "undefined") {
- autoAnnotation = false;
- Zotero.Prefs.set("Knowledge4Zotero.autoAnnotation", autoAnnotation);
- }
-
- const menuitem = _window.document.getElementById(
- "menu_autoannotation_betternotes"
- ) as XUL.MenuItem;
- if (autoAnnotation) {
- menuitem.setAttribute("checked", true);
- } else {
- menuitem.removeAttribute("checked");
- }
-
- // Hide main window menu if the standalone window is already opened
- window.document.getElementById("menu_autoannotation_betternotes").hidden =
- _window !== window;
- }
-
- public async openSelectItemsWindow(): Promise {
- const io = {
- // Not working
- singleSelection: true,
- dataIn: null,
- dataOut: null,
- deferred: Zotero.Promise.defer(),
- };
-
- (window as unknown as XUL.XULWindow).openDialog(
- "chrome://zotero/content/selectItemsDialog.xul",
- "",
- "chrome,dialog=no,centerscreen,resizable=yes",
- io
- );
- await io.deferred.promise;
- return io.dataOut;
- }
-
- public showProgressWindow(
- header: string,
- context: string,
- type: "default" | "success" | "fail" = "default",
- t: number = 5000
- ) {
- let progressWindow = new Zotero.ProgressWindow({ closeOnClick: true });
- progressWindow.changeHeadline(header);
- progressWindow.progress = new progressWindow.ItemProgress(
- this.progressWindowIcon[type],
- context
- );
- progressWindow.show();
- if (t > 0) {
- progressWindow.startCloseTimer(t);
- }
- return progressWindow;
- }
-
- public changeProgressWindowDescription(progressWindow: any, context: string) {
- if (!progressWindow || progressWindow.closed) {
- return;
- }
- progressWindow.progress._itemText.innerHTML = context;
- }
-
- public async waitProgressWindow(progressWindow) {
- let t = 0;
- // Wait for ready
- while (!progressWindow.progress._itemText && t < 100) {
- t += 1;
- await Zotero.Promise.delay(10);
- }
- return;
- }
-
- public async getProgressDocument(progressWindow): Promise {
- await this.waitProgressWindow(progressWindow);
- return progressWindow.progress._hbox.ownerDocument;
- }
-}
-
-export default ZoteroViews;
diff --git a/start.js b/start.js
deleted file mode 100644
index 08a6b78..0000000
--- a/start.js
+++ /dev/null
@@ -1,6 +0,0 @@
-const { execSync } = require("child_process");
-const { exit } = require("process");
-const { startZotero } = require("./zotero-cmd.json");
-
-execSync(startZotero);
-exit(0);
diff --git a/stop.js b/stop.js
deleted file mode 100644
index 50d11ab..0000000
--- a/stop.js
+++ /dev/null
@@ -1,6 +0,0 @@
-const { execSync } = require("child_process");
-const { killZotero } = require("./zotero-cmd.json");
-
-try {
- execSync(killZotero);
-} catch (e) {}
diff --git a/tsconfig.json b/tsconfig.json
index 0df8650..40c646b 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,13 +1,16 @@
{
"compilerOptions": {
- "module": "CommonJS",
- "target": "ES6",
+ "experimentalDecorators": true,
+ "module": "commonjs",
+ "target": "ES2016",
"resolveJsonModule": true,
+ "skipLibCheck": true,
+ "strict": true
},
"include": [
"src",
"typing",
- "node_modules/zotero-types",
+ "node_modules/zotero-types"
],
"exclude": [
"builds",
diff --git a/typing/editor.d.ts b/typing/editor.d.ts
new file mode 100644
index 0000000..592c2ef
--- /dev/null
+++ b/typing/editor.d.ts
@@ -0,0 +1,55 @@
+declare interface EditorCore {
+ debouncedUpdate: Function;
+ disableDrag: boolean;
+ docChanged: boolean;
+ isAttachmentNote: false;
+ metadata: {
+ _citationItems: { itemData: { [k: string]: any } }[];
+ uris: string[];
+ };
+ nodeViews: any[];
+ onUpdateState: Function;
+ options: {
+ isAttachmentNote: false;
+ onImportImages: Function;
+ onInsertObject: Function;
+ onOpenAnnotation: Function;
+ onOpenCitationPage: Function;
+ onOpenCitationPopup: Function;
+ onOpenContextMenu: Function;
+ onOpenURL: Function;
+ onShowCitationItem: Function;
+ onSubscribe: Function;
+ onUnsubscribe: Function;
+ onUpdate: Function;
+ onUpdateCitationItemsList: Function;
+ placeholder: boolean;
+ readOnly: boolean;
+ reloaded: boolean;
+ smartQuotes: boolean;
+ unsaved: boolean;
+ value: string;
+ };
+ pluginState: { [k: string]: any };
+ provider: import("react").Provider;
+ readOnly: boolean;
+ reloaded: boolean;
+ view: import("prosemirror-view").EditorView & {
+ docView: NodeViewDesc;
+ };
+}
+
+declare type EditorAPI =
+ typeof import("../src/extras/editorScript").BetterNotesEditorAPI;
+
+declare interface EditorElement extends XUL.Box {
+ _iframe: HTMLIFrameElement;
+ _editorInstance: Zotero.EditorInstance;
+ _initialized?: boolean;
+ mode?: "edit" | "view";
+ viewMode?: string;
+ parent?: Zotero.Item;
+ item?: Zotero.Item;
+ getCurrentInstance(): Zotero.EditorInstance;
+ initEditor(): Promise;
+}
diff --git a/typing/global.d.ts b/typing/global.d.ts
index 6ec5284..afdd36b 100644
--- a/typing/global.d.ts
+++ b/typing/global.d.ts
@@ -1,49 +1,32 @@
-declare interface SyncStatus {
- path: string;
- filename: string;
- md5: string;
- noteMd5: string;
- lastsync: number;
- itemID: number;
+declare const _globalThis: {
+ [key: string]: any;
+ Zotero: _ZoteroTypes.Zotero;
+ ZoteroPane: _ZoteroTypes.ZoteroPane;
+ Zotero_Tabs: typeof Zotero_Tabs;
+ window: Window;
+ document: Document;
+ OS: typeof OS;
+ Blob: typeof Blob;
+ ztoolkit: typeof ztoolkit;
+ addon: typeof addon;
+};
+
+declare interface Window {
+ openDialog(
+ url?: string | URL,
+ target?: string,
+ features?: string,
+ ...args: any
+ ): Window;
}
-declare interface MDStatus {
- meta: {
- version: number;
- } | null;
- content: string;
- filedir: string;
- filename: string;
- lastmodify: Date;
-}
+// declare const ztoolkit: import("../src/addon").MyToolkit;
+declare const ztoolkit: import("zotero-plugin-toolkit").ZoteroToolkit;
-declare interface NoteStatus {
- meta: string;
- content: string;
- tail: string;
- lastmodify: Date;
-}
+declare const rootURI: string;
-declare interface AnnotationJson {
- authorName: string;
- color: string;
- comment: string;
- dateModified: string;
- image: string;
- imageAttachmentKey: string;
- isAuthorNameAuthoritative: boolean;
- isExternal: boolean;
- id: string;
- key: string;
- lastModifiedByUser: string;
- pageLabel: string;
- position: {
- rects: number[];
- };
- readOnly: boolean;
- sortIndex: any;
- tags: { name: string }[];
- text: string;
- type: string;
- attachmentItemID: number;
-}
+declare const addon: import("../src/addon").default;
+
+declare const __env__: "production" | "development";
+
+declare const ChromeUtils: any;
diff --git a/typing/note.d.ts b/typing/note.d.ts
new file mode 100644
index 0000000..dc41334
--- /dev/null
+++ b/typing/note.d.ts
@@ -0,0 +1,34 @@
+declare interface NoteNodeData {
+ id: number;
+ level: number;
+ name: string;
+ lineIndex: number;
+ endIndex: number;
+ link: string;
+}
+
+declare interface NoteStatus {
+ meta: string;
+ content: string;
+ tail: string;
+ lastmodify: Date;
+}
+
+declare interface SyncStatus {
+ path: string;
+ filename: string;
+ md5: string;
+ noteMd5: string;
+ lastsync: number;
+ itemID: number;
+}
+
+declare interface MDStatus {
+ meta: {
+ version: number;
+ } | null;
+ content: string;
+ filedir: string;
+ filename: string;
+ lastmodify: Date;
+}
diff --git a/typing/template.ts b/typing/template.ts
new file mode 100644
index 0000000..3726d5c
--- /dev/null
+++ b/typing/template.ts
@@ -0,0 +1,4 @@
+declare interface NoteTemplate {
+ name: string;
+ text: string;
+}
diff --git a/update-template.json b/update-template.json
new file mode 100644
index 0000000..7dca98e
--- /dev/null
+++ b/update-template.json
@@ -0,0 +1,26 @@
+{
+ "addons": {
+ "__addonID__": {
+ "updates": [
+ {
+ "version": "__buildVersion__",
+ "update_link": "__releasepage__",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "60.0"
+ }
+ }
+ },
+ {
+ "version": "__buildVersion__",
+ "update_link": "__releasepage__",
+ "applications": {
+ "zotero": {
+ "strict_min_version": "6.999"
+ }
+ }
+ }
+ ]
+ }
+ }
+}
diff --git a/update-template.rdf b/update-template.rdf
new file mode 100644
index 0000000..b82d92f
--- /dev/null
+++ b/update-template.rdf
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+ __buildVersion__
+
+
+ zotero@chnm.gmu.edu
+ 6.999
+ *
+ __releasepage__
+
+
+
+
+ juris-m@juris-m.github.io
+ 6.999
+ *
+ __releasepage__
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/update.json b/update.json
new file mode 100644
index 0000000..1c1fd0f
--- /dev/null
+++ b/update.json
@@ -0,0 +1,26 @@
+{
+ "addons": {
+ "Knowledge4Zotero@windingwind.com": {
+ "updates": [
+ {
+ "version": "0.8.8",
+ "update_link": "__releasepage__",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "60.0"
+ }
+ }
+ },
+ {
+ "version": "0.8.8",
+ "update_link": "__releasepage__",
+ "applications": {
+ "zotero": {
+ "strict_min_version": "6.999"
+ }
+ }
+ }
+ ]
+ }
+ }
+}
diff --git a/update.rdf b/update.rdf
index 8204d94..5e2e88a 100644
--- a/update.rdf
+++ b/update.rdf
@@ -1,25 +1,25 @@
-
+
- 0.8.9
+ 0.8.8
zotero@chnm.gmu.edu
- 6.0.14-beta
+ 6.999
*
- https://github.com/windingwind/zotero-better-notes/releases/latest/download/zotero-better-notes.xpi
+ __releasepage__
juris-m@juris-m.github.io
- 6.0.14-beta
+ 6.999
*
- https://github.com/windingwind/zotero-better-notes/releases/latest/download/zotero-better-notes.xpi
+ __releasepage__
@@ -27,4 +27,4 @@
-
+
\ No newline at end of file
diff --git a/zotero-cmd-default.json b/zotero-cmd-default.json
deleted file mode 100644
index ad72ffc..0000000
--- a/zotero-cmd-default.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "usage": "Copy and rename this file to zotero-cmd.json. Edit the cmd.",
- "killZotero": "taskkill /f /im zotero.exe",
- "startZotero": "/path/to/zotero.exe --debugger --purgecaches"
-}