diff --git a/addon/chrome/content/diff.html b/addon/chrome/content/diff.html new file mode 100644 index 0000000..a66f76b --- /dev/null +++ b/addon/chrome/content/diff.html @@ -0,0 +1,415 @@ + + +
+ + + + + + +s around - // "paragraphs" that are wrapped in non-block-level tags, such as anchors, - // phrase emphasis, and spans. The list of tags we're looking for is - // hard-coded: - var block_tags_a = - "p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del"; - var block_tags_b = - "p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math"; - - // First, look for nested blocks, e.g.: - //
tags around block-level tags.
- text = _HashHTMLBlocks(text);
- text = _FormParagraphs(text, doNotUnhash);
-
- return text;
- }
-
- function _RunSpanGamut(text) {
- //
- // These are all the transformations that occur *within* block-level
- // tags like paragraphs, headers, and list items.
- //
-
- text = _DoCodeSpans(text);
- text = _EscapeSpecialCharsWithinTagAttributes(text);
- text = _EncodeBackslashEscapes(text);
-
- // Process anchor and image tags. Images must come first,
- // because ![foo][f] looks like an anchor.
- text = _DoImages(text);
- text = _DoAnchors(text);
-
- // Make links out of things like ` Just type tags
- //
-
- // Strip leading and trailing lines:
- text = text.replace(/^\n+/g, "");
- text = text.replace(/\n+$/g, "");
-
- var grafs = text.split(/\n{2,}/g);
- var grafsOut = [];
-
- var markerRe = /~K(\d+)K/;
-
- //
- // Wrap tags.
- //
- var end = grafs.length;
- for (var i = 0; i < end; i++) {
- var str = grafs[i];
-
- // if this is an HTML marker, copy it
- if (markerRe.test(str)) {
- grafsOut.push(str);
- } else if (/\S/.test(str)) {
- str = _RunSpanGamut(str);
- str = str.replace(/^([ \t]*)/g, " ");
- str += " {{image}} ${formatted}
+ 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(/(^ (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;
+
+ 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);
+ let rehype = await this.remark2rehype(remark);
+ const html = this.rehype2note(rehype);
+ 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 nodes;
+ }
+
+ getN2MRehypeCitationNodes(rehype) {
+ const nodes = [];
+ visit(
+ rehype,
+ (node: any) =>
+ node.type === "element" &&
+ node.properties?.className?.includes("citation"),
+ (node) => nodes.push(node)
+ );
+ return 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)
+ );
+ return nodes;
+ }
+
+ getN2MRehypeImageNodes(rehype) {
+ const nodes = [];
+ visit(
+ rehype,
+ (node: any) =>
+ node.type === "element" &&
+ node.tagName === "img" &&
+ node.properties?.dataAttachmentKey,
+ (node) => nodes.push(node)
+ );
+ return 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;
+ Zotero.debug("----Debug Link----");
+ Zotero.debug(annotation);
+ console.log("convertAnnotations", node);
+
+ if (typeof uri === "string" && typeof position === "object") {
+ Zotero.debug(uri);
+ let openURI;
+ let uriParts = uri.split("/");
+ let libraryType = uriParts[3];
+ let key = uriParts[uriParts.length - 1];
+ Zotero.debug(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 }, toText(node))];
+ newChild.properties.ztype = "zhighlight";
+ newNode = h("zhighlight", [newChild]);
+ }
+ console.log(newNode, node);
+ this.replace(node, newNode);
+ console.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];
+ Zotero.debug(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);
+ console.log("cite child", child, newNode);
+ newNode.children = [h("a", { href: uris[i] }, child.children)];
+ return newNode;
+ }),
+ { type: "text", value: ")" },
+ ]);
+ console.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,
+ Info: any,
+ mode: NodeMode = NodeMode.default
+ ) {
+ if (!nodes.length) {
+ return;
+ }
+ for (const node of nodes) {
+ console.log("note link", node);
+ const noteInfo = Info.find((i) => node.properties.href.includes(i.link));
+ const link = `./${noteInfo.filename}`;
+ if (!noteInfo) {
+ continue;
+ }
+
+ 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";
+ const newNode = h("znotelink", [newChild]);
+ this.replace(node, newNode);
+ console.log("direct link", node, newNode, newChild);
+ }
+ console.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
+ );
+ Zotero.debug(attachmentItem);
+ console.log("image", libraryID, imgKey, attachmentItem, node);
+ if (!attachmentItem) {
+ continue;
+ }
+
+ let oldFile = String(await attachmentItem.getFilePathAsync());
+ Zotero.debug(oldFile);
+ let ext = oldFile.split(".").pop();
+ let newAbsPath = Zotero.Knowledge4Zotero.NoteUtils.formatPath(
+ `${Path}/${imgKey}.${ext}`
+ );
+ Zotero.debug(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) {
+ Zotero.debug(e);
+ }
+ Zotero.debug(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);
+ }
+ console.log("zimage", node);
+ }
+ }
+
+ getM2NRehypeAnnotationNodes(rehype) {
+ const nodes = [];
+ visit(
+ rehype,
+ (node: any) => node.type === "element" && node.properties?.dataAnnotation,
+ (node) => nodes.push(node)
+ );
+ return nodes;
+ }
+
+ getM2NRehypeHighlightNodes(rehype) {
+ const nodes = [];
+ visit(
+ rehype,
+ (node: any) =>
+ node.type === "element" && node.properties?.ztype === "zhighlight",
+ (node) => nodes.push(node)
+ );
+ console.log("N2M:highlight", nodes);
+ return nodes;
+ }
+
+ getM2NRehypeCitationNodes(rehype) {
+ const nodes = [];
+ visit(
+ rehype,
+ (node: any) =>
+ node.type === "element" &&
+ (node.properties?.ztype === "zcitation" ||
+ node.properties?.dataCitation),
+ (node) => nodes.push(node)
+ );
+ return nodes;
+ }
+
+ getM2NRehypeNoteLinkNodes(rehype) {
+ const nodes = [];
+ visit(
+ rehype,
+ (node: any) =>
+ node.type === "element" && node.properties?.ztype === "znotelink",
+ (node) => nodes.push(node)
+ );
+ return nodes;
+ }
+
+ getM2NRehypeImageNodes(rehype) {
+ const nodes = [];
+ visit(
+ rehype,
+ (node: any) => node.type === "element" && node.tagName === "img",
+ (node) => nodes.push(node)
+ );
+ return nodes;
+ }
+
+ processM2NRehypeHighlightNodes(nodes) {
+ if (!nodes.length) {
+ return;
+ }
+ for (const node of nodes) {
+ node.children = [{ type: "text", value: toText(node) }];
+ 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) {
+ Zotero.debug(e);
+ console.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.zhref;
+ delete node.properties.ztype;
+ }
+ }
+
+ async processM2NRehypeImageNodes(
+ nodes: any[],
+ noteItem: Zotero.Item,
+ fileDir: string,
+ isImport: boolean = false
+ ) {
+ if (!nodes.length || (isImport && !noteItem)) {
+ return;
+ }
+
+ console.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))) {
+ Zotero.debug("BN:parse image, path invalid");
+ continue;
+ }
+ }
+ }
+ const key = await (
+ Zotero.Knowledge4Zotero as Knowledge4Zotero
+ ).NoteUtils._importImage(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/templateWindow.ts b/src/template/templateWindow.ts
index 9d47562..1b455fe 100644
--- a/src/template/templateWindow.ts
+++ b/src/template/templateWindow.ts
@@ -137,8 +137,8 @@ class TemplateWindow extends AddonBase {
);
await io.deferred.promise;
- const ids = io.dataOut;
- const note: Zotero.Item = (Zotero.Items.get(ids) as Zotero.Item[]).filter(
+ const ids = io.dataOut as number[];
+ const note: Zotero.Item = Zotero.Items.get(ids).filter(
(item: Zotero.Item) => item.isNote()
)[0];
if (!note) {
diff --git a/src/utils.ts b/src/utils.ts
index e62d6c2..b96b9b1 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -109,7 +109,7 @@ async function pick(
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
- return new Zotero.Promise((resolve) => {
+ return new Promise((resolve) => {
fp.open((userChoice) => {
switch (userChoice) {
case Components.interfaces.nsIFilePicker.returnOK:
@@ -124,4 +124,38 @@ async function pick(
});
});
}
-export { EditorMessage, OutlineType, NoteTemplate, CopyHelper, pick };
+
+enum SyncCode {
+ UpToDate = 0,
+ NoteAhead,
+ MDAhead,
+ NeedDiff,
+}
+
+enum NodeMode {
+ default = 0,
+ wrap,
+ replace,
+ direct,
+}
+
+function getDOMParser(): DOMParser {
+ if (Zotero.platformMajorVersion > 60) {
+ return new DOMParser();
+ } else {
+ return Components.classes[
+ "@mozilla.org/xmlextras/domparser;1"
+ ].createInstance(Components.interfaces.nsIDOMParser);
+ }
+}
+
+export {
+ EditorMessage,
+ OutlineType,
+ NoteTemplate,
+ CopyHelper,
+ pick,
+ SyncCode,
+ NodeMode,
+ getDOMParser,
+};
diff --git a/src/workspace/workspaceWindow.ts b/src/workspace/workspaceWindow.ts
index 1a29f20..c262888 100644
--- a/src/workspace/workspaceWindow.ts
+++ b/src/workspace/workspaceWindow.ts
@@ -7,13 +7,13 @@ import { EditorMessage, OutlineType, pick } from "../utils";
import AddonBase from "../module";
class WorkspaceWindow extends AddonBase {
- private _initIframe: ZoteroPromise;
+ private _initIframe: _ZoteroPromise;
public workspaceWindow: Window;
public workspaceTabId: string;
public workspaceNoteEditor: Zotero.EditorInstance | undefined;
public previewItemID: number;
private _firstInit: boolean;
- public _workspacePromise: ZoteroPromise;
+ public _workspacePromise: _ZoteroPromise;
private _DOMParser: any;
constructor(parent: Knowledge4Zotero) {
diff --git a/src/zotero/events.ts b/src/zotero/events.ts
index 0219e11..7485abe 100644
--- a/src/zotero/events.ts
+++ b/src/zotero/events.ts
@@ -30,10 +30,14 @@ class ZoteroEvents extends AddonBase {
this._Addon.ZoteroViews.updateWordCount();
}
// Check Note Sync
- const syncIds =
- this._Addon.SyncController.getSyncNoteIds() as number[];
- if (ids.filter((id) => syncIds.includes(id as number)).length > 0) {
- this._Addon.SyncController.setSync();
+ const syncIds = this._Addon.SyncController.getSyncNoteIds();
+ const modifiedSyncIds = ids.filter((id) =>
+ syncIds.includes(id as number)
+ ) as number[];
+ if (modifiedSyncIds.length > 0) {
+ this._Addon.SyncController.doSync(
+ Zotero.Items.get(modifiedSyncIds)
+ );
Zotero.debug("Better Notes: sync planned.");
}
}
@@ -323,6 +327,7 @@ class ZoteroEvents extends AddonBase {
instance._iframeWindow.document.addEventListener(
"selectionchange",
async (e) => {
+ e.stopPropagation();
await this._Addon.NoteUtils.onSelectionChange(instance);
}
);
@@ -924,7 +929,10 @@ class ZoteroEvents extends AddonBase {
console.log(html);
new CopyHelper()
.addText(html, "text/html")
- .addText(this._Addon.NoteParse.parseHTMLToMD(html), "text/unicode")
+ .addText(
+ await this._Addon.NoteParse.parseHTMLToMD(html),
+ "text/unicode"
+ )
.copy();
progressWindow.changeHeadline("Template Copied");
} else {
@@ -1040,7 +1048,10 @@ class ZoteroEvents extends AddonBase {
new CopyHelper()
.addText(html, "text/html")
- .addText(this._Addon.NoteParse.parseHTMLToMD(html), "text/unicode")
+ .addText(
+ await this._Addon.NoteParse.parseHTMLToMD(html),
+ "text/unicode"
+ )
.copy();
progressWindow.changeHeadline("Template Copied");
} else {
@@ -1119,11 +1130,10 @@ class ZoteroEvents extends AddonBase {
return;
}
if (options.exportMD && options.exportSubMD) {
- await this._Addon.NoteExport.exportNotesToMDFiles(
- [item],
- false,
- options.exportAutoSync
- );
+ await this._Addon.NoteExport.exportNotesToMDFiles([item], {
+ useEmbed: false,
+ useSync: options.exportAutoSync,
+ });
} else {
await this._Addon.NoteExport.exportNote(item, options);
}
@@ -1156,10 +1166,9 @@ class ZoteroEvents extends AddonBase {
);
} else {
const useSingleFile = confirm("Export linked notes to markdown files?");
- await this._Addon.NoteExport.exportNotesToMDFiles(
- noteItems,
- !useSingleFile
- );
+ await this._Addon.NoteExport.exportNotesToMDFiles(noteItems, {
+ useEmbed: !useSingleFile,
+ });
}
} else if (message.type === "sync") {
/*
@@ -1169,9 +1178,12 @@ class ZoteroEvents extends AddonBase {
*/
const note = this._Addon.WorkspaceWindow.getWorkspaceNote();
if (this._Addon.SyncController.isSyncNote(note)) {
- this._Addon.SyncController.doSync([note], true, false);
+ this._Addon.SyncController.doSync([note]);
} else {
- await this._Addon.NoteExport.exportNotesToMDFiles([note], false, true);
+ await this._Addon.NoteExport.exportNotesToMDFiles([note], {
+ useEmbed: false,
+ useSync: true,
+ });
}
} else if (message.type === "openAttachment") {
/*
@@ -1262,7 +1274,7 @@ class ZoteroEvents extends AddonBase {
);
return;
}
- const html = this._Addon.NoteParse.parseMDToHTML(source);
+ const html = await this._Addon.NoteParse.parseMDToHTML(source);
console.log(source, html);
new CopyHelper().addText(html, "text/html").copy();
diff --git a/src/zotero/views.ts b/src/zotero/views.ts
index 2a9e90d..999434c 100644
--- a/src/zotero/views.ts
+++ b/src/zotero/views.ts
@@ -68,20 +68,56 @@ class ZoteroViews extends AddonBase {
let addNoteItem = document
.getElementById("zotero-tb-note-add")
.getElementsByTagName("menuitem")[1];
- let button = document.createElement("menuitem");
- button.setAttribute("id", "zotero-tb-knowledge-openwindow");
- button.setAttribute("label", "New Main Note");
- button.addEventListener("click", (e) => {
- this._Addon.ZoteroEvents.onEditorEvent(
- new EditorMessage("createWorkspace", {})
- );
+ let buttons = this.createXULElement(document, {
+ tag: "fragment",
+ subElementOptions: [
+ {
+ tag: "menuitem",
+ id: "zotero-tb-knowledge-create-mainnote",
+ attributes: [
+ ["label", "New Main Note"],
+ ["class", "menuitem-iconic"],
+ [
+ "style",
+ "list-style-image: url('chrome://Knowledge4Zotero/skin/favicon.png');",
+ ],
+ ],
+ listeners: [
+ [
+ "click",
+ (e) => {
+ this._Addon.ZoteroEvents.onEditorEvent(
+ new EditorMessage("createWorkspace", {})
+ );
+ },
+ false,
+ ],
+ ],
+ },
+ {
+ tag: "menuitem",
+ id: "zotero-tb-knowledge-import-md",
+ attributes: [
+ ["label", "Import MarkDown as Note"],
+ ["class", "menuitem-iconic"],
+ [
+ "style",
+ "list-style-image: url('chrome://Knowledge4Zotero/skin/favicon.png');",
+ ],
+ ],
+ listeners: [
+ [
+ "click",
+ async (e) => {
+ await this._Addon.NoteImport.doImport();
+ },
+ false,
+ ],
+ ],
+ },
+ ],
});
- button.setAttribute("class", "menuitem-iconic");
- button.setAttribute(
- "style",
- "list-style-image: url('chrome://Knowledge4Zotero/skin/favicon.png');"
- );
- addNoteItem.after(button);
+ addNoteItem.after(buttons);
}
public addOpenWorkspaceButton() {
@@ -111,10 +147,12 @@ class ZoteroViews extends AddonBase {
: "Open Workspace";
span1.append(span2, span3, span4);
treeRow.append(span1);
- treeRow.addEventListener("click", (e) => {
- this._Addon.ZoteroEvents.onEditorEvent(
- new EditorMessage("openWorkspace", { event: e })
- );
+ treeRow.addEventListener("click", async (e) => {
+ if (e.shiftKey) {
+ await this._Addon.WorkspaceWindow.openWorkspaceWindow("window", true);
+ } else {
+ await this._Addon.WorkspaceWindow.openWorkspaceWindow();
+ }
});
treeRow.addEventListener("mouseover", (e: XUL.XULEvent) => {
treeRow.setAttribute(
@@ -361,6 +399,16 @@ class ZoteroViews extends AddonBase {
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 createXULElement(doc: Document, options: XULElementOptions) {
const createElement: () => XUL.Element =
options.tag === "fragment"
diff --git a/typing/global.d.ts b/typing/global.d.ts
index dba0b8f..064f6e9 100644
--- a/typing/global.d.ts
+++ b/typing/global.d.ts
@@ -1,8 +1,3 @@
-declare interface ZoteroPromise {
- promise: Promise
\n");
-
- return text;
- }
-
- function _EscapeSpecialCharsWithinTagAttributes(text) {
- //
- // Within tags -- meaning between < and > -- encode [\ ` * _] so they
- // don't conflict with their use in Markdown for code, italics and strong.
- //
-
- // Build a regex to find HTML tags and comments. See Friedl's
- // "Mastering Regular Expressions", 2nd Ed., pp. 200-201.
-
- // SE: changed the comment part of the regex
-
- var regex =
- /(<[a-z\/!$]("[^"]*"|'[^']*'|[^'">])*>|-]|-[^>])(?:[^-]|-[^-])*)--)>)/gi;
-
- text = text.replace(regex, function (wholeMatch) {
- var tag = wholeMatch.replace(/(.)<\/?code>(?=.)/g, "$1`");
- tag = escapeCharacters(
- tag,
- wholeMatch.charAt(1) == "!" ? "\\`*_/" : "\\`*_"
- ); // also escape slashes in comments to prevent autolinking there -- http://meta.stackoverflow.com/questions/95987
- return tag;
- });
-
- return text;
- }
-
- function _DoAnchors(text) {
- //
- // Turn Markdown link shortcuts into XHTML tags.
- //
- //
- // First, handle reference-style links: [link text] [id]
- //
-
- /*
- text = text.replace(/
- ( // wrap whole match in $1
- \[
- (
- (?:
- \[[^\]]*\] // allow brackets nested one level
- |
- [^\[] // or anything else
- )*
- )
- \]
- [ ]? // one optional space
- (?:\n[ ]*)? // one optional newline followed by spaces
- \[
- (.*?) // id = $3
- \]
- )
- ()()()() // pad remaining backreferences
- /g, writeAnchorTag);
- */
- text = text.replace(
- /(\[((?:\[[^\]]*\]|[^\[\]])*)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g,
- writeAnchorTag
- );
-
- //
- // Next, inline-style links: [link text](url "optional title")
- //
-
- /*
- text = text.replace(/
- ( // wrap whole match in $1
- \[
- (
- (?:
- \[[^\]]*\] // allow brackets nested one level
- |
- [^\[\]] // or anything else
- )*
- )
- \]
- \( // literal paren
- [ \t]*
- () // no id, so leave $3 empty
- ( // href = $4
- (?:
- \([^)]*\) // allow one level of (correctly nested) parens (think MSDN)
- |
- [^()\s]
- )*?
- )>?
- [ \t]*
- ( // $5
- (['"]) // quote char = $6
- (.*?) // Title = $7
- \6 // matching quote
- [ \t]* // ignore any spaces/tabs between closing quote and )
- )? // title is optional
- \)
- )
- /g, writeAnchorTag);
- */
-
- text = text.replace(
- /(\[((?:\[[^\]]*\]|[^\[\]])*)\]\([ \t]*()((?:\([^)]*\)|[^()\s])*?)>?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g,
- writeAnchorTag
- );
-
- //
- // Last, handle reference-style shortcuts: [link text]
- // These must come last in case you've also got [link test][1]
- // or [link test](/foo)
- //
-
- /*
- text = text.replace(/
- ( // wrap whole match in $1
- \[
- ([^\[\]]+) // link text = $2; can't contain '[' or ']'
- \]
- )
- ()()()()() // pad rest of backreferences
- /g, writeAnchorTag);
- */
- text = text.replace(/(\[([^\[\]]+)\])()()()()()/g, writeAnchorTag);
-
- return text;
- }
-
- function writeAnchorTag(wholeMatch, m1, m2, m3, m4, m5, m6, m7) {
- if (m7 == undefined) m7 = "";
- var whole_match = m1;
- var link_text = m2.replace(/:\/\//g, "~P"); // to prevent auto-linking withing the link. will be converted back after the auto-linker runs
- var link_id = m3.toLowerCase();
- var url = m4;
- var title = m7;
-
- if (url == "") {
- if (link_id == "") {
- // lower-case and turn embedded newlines into spaces
- link_id = link_text.toLowerCase().replace(/ ?\n/g, " ");
- }
- url = "#" + link_id;
-
- if (g_urls.get(link_id) != undefined) {
- url = g_urls.get(link_id);
- if (g_titles.get(link_id) != undefined) {
- title = g_titles.get(link_id);
- }
- } else {
- if (whole_match.search(/\(\s*\)$/m) > -1) {
- // Special case for explicit empty url
- url = "";
- } else {
- return whole_match;
- }
- }
- }
- url = encodeProblemUrlChars(url);
- url = escapeCharacters(url, "*_");
- var result = '" + link_text + "";
-
- return result;
- }
-
- function _DoImages(text) {
- //
- // Turn Markdown image shortcuts into tags.
- //
-
- //
- // First, handle reference-style labeled images: ![alt text][id]
- //
-
- /*
- text = text.replace(/
- ( // wrap whole match in $1
- !\[
- (.*?) // alt text = $2
- \]
- [ ]? // one optional space
- (?:\n[ ]*)? // one optional newline followed by spaces
- \[
- (.*?) // id = $3
- \]
- )
- ()()()() // pad rest of backreferences
- /g, writeImageTag);
- */
- text = text.replace(
- /(!\[(.*?)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g,
- writeImageTag
- );
-
- //
- // Next, handle inline images: 
- // Don't forget: encode * and _
-
- /*
- text = text.replace(/
- ( // wrap whole match in $1
- !\[
- (.*?) // alt text = $2
- \]
- \s? // One optional whitespace character
- \( // literal paren
- [ \t]*
- () // no id, so leave $3 empty
- (\S+?)>? // src url = $4
- [ \t]*
- ( // $5
- (['"]) // quote char = $6
- (.*?) // title = $7
- \6 // matching quote
- [ \t]*
- )? // title is optional
- \)
- )
- /g, writeImageTag);
- */
- text = text.replace(
- /(!\[(.*?)\]\s?\([ \t]*()(\S+?)>?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g,
- writeImageTag
- );
-
- return text;
- }
-
- function attributeEncode(text) {
- // unconditionally replace angle brackets here -- what ends up in an attribute (e.g. alt or title)
- // never makes sense to have verbatim HTML in it (and the sanitizer would totally break it)
- return text
- .replace(/>/g, ">")
- .replace(/";
-
- return result;
- }
-
- function _DoHeaders(text) {
- // Setext-style headers:
- // Header 1
- // ========
- //
- // Header 2
- // --------
- //
- text = text.replace(
- /^(.+)[ \t]*\n=+[ \t]*\n+/gm,
- function (wholeMatch, m1) {
- return "
" + _RunSpanGamut(m1) + "
\n\n";
- }
- );
-
- text = text.replace(
- /^(.+)[ \t]*\n-+[ \t]*\n+/gm,
- function (matchFound, m1) {
- return "" + _RunSpanGamut(m1) + "
\n\n";
- }
- );
-
- // atx-style headers:
- // # Header 1
- // ## Header 2
- // ## Header 2 with closing hashes ##
- // ...
- // ###### Header 6
- //
-
- /*
- text = text.replace(/
- ^(\#{1,6}) // $1 = string of #'s
- [ \t]*
- (.+?) // $2 = Header text
- [ \t]*
- \#* // optional closing #'s (not counted)
- \n+
- /gm, function() {...});
- */
-
- text = text.replace(
- /^(\#{1,6})[ \t]*(.+?)[ \t]*\#*\n+/gm,
- function (wholeMatch, m1, m2) {
- var h_level = m1.length;
- return (
- "` blocks.
- //
-
- /*
- text = text.replace(/
- (?:\n\n|^)
- ( // $1 = the code block -- one or more lines, starting with a space/tab
- (?:
- (?:[ ]{4}|\t) // Lines must start with a tab or a tab-width of spaces - attacklab: g_tab_width
- .*\n+
- )+
- )
- (\n*[ ]{0,3}[^ \t\n]|(?=~0)) // attacklab: g_tab_width
- /g ,function(){...});
- */
-
- // attacklab: sentinel workarounds for lack of \A and \Z, safari\khtml bug
- text += "~0";
-
- text = text.replace(
- /(?:\n\n|^)((?:(?:[ ]{4}|\t).*\n+)+)(\n*[ ]{0,3}[^ \t\n]|(?=~0))/g,
- function (wholeMatch, m1, m2) {
- var codeblock = m1;
- var nextChar = m2;
-
- codeblock = _EncodeCode(_Outdent(codeblock));
- codeblock = _Detab(codeblock);
- codeblock = codeblock.replace(/^\n+/g, ""); // trim leading newlines
- codeblock = codeblock.replace(/\n+$/g, ""); // trim trailing whitespace
-
- codeblock = "
";
-
- return "\n\n" + codeblock + "\n\n" + nextChar;
- }
- );
-
- // attacklab: strip sentinel
- text = text.replace(/~0/, "");
-
- return text;
- }
-
- function hashBlock(text) {
- text = text.replace(/(^\n+|\n+$)/g, "");
- return "\n\n~K" + (g_html_blocks.push(text) - 1) + "K\n\n";
- }
-
- function _DoCodeSpans(text) {
- //
- // * Backtick quotes are used for " + codeblock + "\n spans.
- //
- // * You can use multiple backticks as the delimiters if you want to
- // include literal backticks in the code span. So, this input:
- //
- // Just type ``foo `bar` baz`` at the prompt.
- //
- // Will translate to:
- //
- // foo `bar` baz at the prompt.`bar` ...
- //
-
- /*
- text = text.replace(/
- (^|[^\\]) // Character before opening ` can't be a backslash
- (`+) // $2 = Opening run of `
- ( // $3 = The code block
- [^\r]*?
- [^`] // attacklab: work around lack of lookbehind
- )
- \2 // Matching closer
- (?!`)
- /gm, function(){...});
- */
-
- text = text.replace(
- /(^|[^\\])(`+)([^\r]*?[^`])\2(?!`)/gm,
- function (wholeMatch, m1, m2, m3, m4) {
- var c = m3;
- c = c.replace(/^([ \t]*)/g, ""); // leading whitespace
- c = c.replace(/[ \t]*$/g, ""); // trailing whitespace
- c = _EncodeCode(c);
- c = c.replace(/:\/\//g, "~P"); // to prevent auto-linking. Not necessary in code *blocks*, but in code spans. Will be converted back after the auto-linker runs.
- return m1 + "" + c + "";
- }
- );
-
- return text;
- }
-
- function _EncodeCode(text) {
- //
- // Encode/escape certain characters inside Markdown code runs.
- // The point is that in code, these characters are literals,
- // and lose their special Markdown meanings.
- //
- // Encode all ampersands; HTML entities are not
- // entities within a Markdown code span.
- text = text.replace(/&/g, "&");
-
- // Do the angle bracket song and dance:
- text = text.replace(//g, ">");
-
- // Now, escape characters that are magic in Markdown:
- text = escapeCharacters(text, "*_{}[]\\", false);
-
- // jj the line above breaks this:
- //---
-
- //* Item
-
- // 1. Subitem
-
- // special char: *
- //---
-
- return text;
- }
-
- function _DoItalicsAndBold(text) {
- // must go first:
- text = text.replace(
- /([\W_]|^)(\*\*|__)(?=\S)([^\r]*?\S[\*_]*)\2([\W_]|$)/g,
- "$1$3$4"
- );
-
- text = text.replace(
- /([\W_]|^)(\*|_)(?=\S)([^\r\*_]*?\S)\2([\W_]|$)/g,
- "$1$3$4"
- );
-
- return text;
- }
-
- function _DoBlockQuotes(text) {
- /*
- text = text.replace(/
- ( // Wrap whole match in $1
- (
- ^[ \t]*>[ \t]? // '>' at the start of a line
- .+\n // rest of the first line
- (.+\n)* // subsequent consecutive lines
- \n* // blanks
- )+
- )
- /gm, function(){...});
- */
-
- text = text.replace(
- /((^[ \t]*>[ \t]?.+\n(.+\n)*\n*)+)/gm,
- function (wholeMatch, m1) {
- var bq = m1;
-
- // attacklab: hack around Konqueror 3.5.4 bug:
- // "----------bug".replace(/^-/g,"") == "bug"
-
- bq = bq.replace(/^[ \t]*>[ \t]?/gm, "~0"); // trim one level of quoting
-
- // attacklab: clean up hack
- bq = bq.replace(/~0/g, "");
-
- bq = bq.replace(/^[ \t]+$/gm, ""); // trim whitespace-only lines
- bq = _RunBlockGamut(bq); // recurse
-
- bq = bq.replace(/(^|\n)/g, "$1 ");
- // These leading spaces screw with content, so we need to fix that:
- bq = bq.replace(
- /(\s*
[^\r]+?<\/pre>)/gm,
- function (wholeMatch, m1) {
- var pre = m1;
- // attacklab: hack around Konqueror 3.5.4 bug:
- pre = pre.replace(/^ /gm, "~0");
- pre = pre.replace(/~0/g, "");
- return pre;
- }
- );
-
- return hashBlock("\n" + bq + "\n
");
- }
- );
- return text;
- }
-
- function _FormParagraphs(text, doNotUnhash) {
- //
- // Params:
- // $text - string to process with html ]*>`([\s\S]*)`<\/pre>/gi,
- function (str, innerHTML) {
- //innerHTML = innerHTML.replace(/^\t+/g, ' '); // convert tabs to spaces (you know it makes sense)
- innerHTML = innerHTML.replace(/\n/g, "\n ");
- return "\n\n " + innerHTML + "\n";
- }
- );
-
- // Lists
-
- // Escape numbers that could trigger an ol
- string = string.replace(/(\d+). /g, "$1\\. ");
-
- // Converts lists that have no child lists (of same type) first, then works it's way up
- var noChildrenRegex = /<(ul|ol)\b[^>]*>(?:(?!/gi;
- while (string.match(noChildrenRegex)) {
- string = string.replace(noChildrenRegex, function (str) {
- return replaceLists(str);
- });
- }
-
- function replaceLists(html) {
- html = html.replace(
- /<(ul|ol)\b[^>]*>([\s\S]*?)<\/\1>/gi,
- function (str, listType, innerHTML) {
- var lis = innerHTML.split("");
- lis.splice(lis.length - 1, 1);
-
- for (i = 0, len = lis.length; i < len; i++) {
- if (lis[i]) {
- var prefix = listType === "ol" ? i + 1 + ". " : "* ";
- lis[i] = lis[i].replace(
- /\s*
]*>((?:(?!
/gi;
- while (string.match(deepest)) {
- string = string.replace(deepest, function (str) {
- return replaceBlockquotes(str);
- });
- }
-
- function replaceBlockquotes(html) {
- html = html.replace(
- /
]*>([\s\S]*?)<\/blockquote>/gi,
- function (str, inner) {
- inner = inner.replace(/^\s+|\s+$/g, "");
- inner = cleanUp(inner);
- inner = inner.replace(/^/gm, "> ");
- inner = inner.replace(/^(>([ \t]{2,}>)+)/gm, "> >");
- return inner;
- }
- );
- return html;
- }
-
- function cleanUp(string) {
- string = string.replace(/^[\t\r\n]+|[\t\r\n]+$/g, ""); // trim leading/trailing whitespace
- string = string.replace(/\n\s+\n/g, "\n\n");
- string = string.replace(/\n{3,}/g, "\n\n"); // limit consecutive linebreaks to 2
- return string;
- }
-
- return cleanUp(string);
-};
-
-var converter = new Markdown.Converter();
-
-var Markdown2HTML = function (data) {
- return converter.makeHtml(data);
-};
-
-var HTML2Markdown = function (data) {
- return converter.makeMarkdown(data);
-};
-
-export { Markdown2HTML, HTML2Markdown };
diff --git a/src/note/noteExportController.ts b/src/note/noteExportController.ts
index 678d46e..c781b16 100644
--- a/src/note/noteExportController.ts
+++ b/src/note/noteExportController.ts
@@ -14,8 +14,8 @@ class NoteExport extends AddonBase {
note: Zotero.Item;
filename: string;
}>;
- _pdfPrintPromise: ZoteroPromise;
- _docxPromise: ZoteroPromise;
+ _pdfPrintPromise: _ZoteroPromise;
+ _docxPromise: _ZoteroPromise;
_docxBlob: Blob;
constructor(parent: Knowledge4Zotero) {
@@ -182,28 +182,37 @@ class NoteExport extends AddonBase {
async exportNotesToMDFiles(
notes: Zotero.Item[],
- useEmbed: boolean,
- useSync: boolean = false
+ options: {
+ useEmbed?: boolean;
+ useSync?: boolean;
+ filedir?: string;
+ } = {}
) {
Components.utils.import("resource://gre/modules/osfile.jsm");
this._exportFileInfo = [];
- const filepath = await pick(
- Zotero.getString(useSync ? "sync.sync" : "fileInterface.export") +
- " MarkDown",
- "folder"
- );
+ let filedir =
+ options.filedir ||
+ (await pick(
+ Zotero.getString(
+ options.useSync ? "sync.sync" : "fileInterface.export"
+ ) + " MarkDown",
+ "folder"
+ ));
- if (!filepath) {
+ filedir = Zotero.File.normalizeToUnix(filedir);
+
+ if (!filedir) {
+ Zotero.debug("BN:export, filepath invalid");
return;
}
this._exportPath = this._Addon.NoteUtils.formatPath(
- Zotero.File.pathToFile(filepath).path + "/attachments"
+ OS.Path.join(filedir, "attachments")
);
notes = notes.filter((n) => n && n.getNote);
- if (useEmbed) {
+ if (options.useEmbed) {
for (const note of notes) {
let newNote: Zotero.Item;
if (this._Addon.NoteParse.parseLinkInText(note.getNote())) {
@@ -233,9 +242,7 @@ class NoteExport extends AddonBase {
newNote = note;
}
- let filename = `${
- Zotero.File.pathToFile(filepath).path
- }/${await this._getFileName(note)}`;
+ let filename = OS.Path.join(filedir, await this._getFileName(note));
filename = filename.replace(/\\/g, "/");
await this._exportMD(newNote, filename, newNote.id !== note.id);
@@ -243,6 +250,7 @@ class NoteExport extends AddonBase {
} 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
@@ -272,77 +280,38 @@ class NoteExport extends AddonBase {
link: this._Addon.NoteUtils.getNoteLink(_note),
id: _note.id,
note: _note,
- filename: await this._getFileName(_note),
+ filename: await this._getFileName(_note, filedir),
});
}
this._exportFileInfo = noteLinkDict;
for (const noteInfo of noteLinkDict) {
- let exportPath = `${Zotero.File.pathToFile(filepath).path}/${
- noteInfo.filename
- }`;
- await this._exportMD(noteInfo.note, exportPath, false);
- if (useSync) {
- this._Addon.SyncController.updateNoteSyncStatus(
- noteInfo.note,
- Zotero.File.pathToFile(filepath).path,
- noteInfo.filename
- );
+ 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);
+ 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
+ ),
+ lastsync: new Date().getTime(),
+ itemID: noteInfo.id,
+ });
}
}
}
}
- async syncNotesToMDFiles(notes: Zotero.Item[], filepath: string) {
- this._exportPath = this._Addon.NoteUtils.formatPath(
- Zotero.File.pathToFile(filepath).path + "/attachments"
- );
-
- // Export every linked note as a markdown file
- // Find all linked notes that need to be exported
- 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 = new Array(...new Set(allNoteIds));
- // console.log(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),
- });
- }
- this._exportFileInfo = noteLinkDict;
-
- for (const note of notes) {
- const syncInfo = this._Addon.SyncController.getNoteSyncStatus(note);
- let exportPath = `${decodeURIComponent(
- syncInfo.path
- )}/${decodeURIComponent(syncInfo.filename)}`;
- await this._exportMD(note, exportPath, false);
- this._Addon.SyncController.updateNoteSyncStatus(note);
- }
- }
-
private async _exportDocx(filename: string) {
await Zotero.File.putContentsAsync(filename, this._docxBlob);
this._Addon.ZoteroViews.showProgressWindow(
@@ -362,7 +331,9 @@ class NoteExport extends AddonBase {
}
filename = this._Addon.NoteUtils.formatPath(filename);
- const content: string = await this._Addon.NoteParse.parseNoteToMD(note);
+ const content: string = await this._Addon.NoteParse.parseNoteToMD(note, {
+ withMeta: true,
+ });
console.log(
`Exporting MD file: ${filename}, content length: ${content.length}`
);
@@ -378,6 +349,7 @@ class NoteExport extends AddonBase {
}
await Zotero.Items.erase(note.id);
}
+ return content;
}
private async _exportFreeMind(noteItem: Zotero.Item, filename: string) {
@@ -392,7 +364,35 @@ class NoteExport extends AddonBase {
);
}
- private async _getFileName(noteItem: Zotero.Item) {
+ 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]",
diff --git a/src/note/noteImportController.ts b/src/note/noteImportController.ts
new file mode 100644
index 0000000..c078b70
--- /dev/null
+++ b/src/note/noteImportController.ts
@@ -0,0 +1,99 @@
+/*
+ * This file realizes md import.
+ */
+
+import Knowledge4Zotero from "../addon";
+import AddonBase from "../module";
+import { pick } from "../utils";
+
+class NoteImport extends AddonBase {
+ constructor(parent: Knowledge4Zotero) {
+ super(parent);
+ }
+
+ async doImport(
+ noteItem: Zotero.Item = undefined,
+ options: {
+ ignoreVersion?: boolean;
+ append?: boolean;
+ } = {}
+ ) {
+ const filepath = await pick(
+ `${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) {
+ Zotero.debug(`BN 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: '
`;
+ }
+
+ // 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 = "
{{citation}} {{comment}}[^<>]*?)({{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;
}
- let annotationJSONList = [];
+ 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) {
@@ -288,10 +433,45 @@ class NoteParse extends AddonBase {
annotationJSONList.push(annotJson);
}
await this._Addon.NoteUtils.importImagesToNote(note, annotationJSONList);
- const html =
- Zotero.EditorInstanceUtilities.serializeAnnotations(
- annotationJSONList
- ).html;
+ 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 += `
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