From 71b961512d0b56e40cd732383bb9d7e6075165ac Mon Sep 17 00:00:00 2001
From: xiangyu <3170102889@zju.edu.cn>
Date: Wed, 1 Jun 2022 22:51:25 +0800
Subject: [PATCH] update: rewrite the markdown export translator
fix: #28 Markdown convert bug
---
addon/chrome/content/export.xul | 4 -
addon/chrome/locale/en-US/overlay.dtd | 3 +-
addon/chrome/locale/zh-CN/overlay.dtd | 3 +-
build.js | 5 +
src/Better Note Markdown.js | 1809 +++++++++++++++++++++++++
src/events.ts | 4 +-
src/export.ts | 15 -
src/exportMD.ts | 34 +
src/file_picker.ts | 33 +
src/knowledge.ts | 202 +--
10 files changed, 1955 insertions(+), 157 deletions(-)
create mode 100644 src/Better Note Markdown.js
create mode 100644 src/exportMD.ts
create mode 100644 src/file_picker.ts
diff --git a/addon/chrome/content/export.xul b/addon/chrome/content/export.xul
index b5e4fc2..8c2c529 100644
--- a/addon/chrome/content/export.xul
+++ b/addon/chrome/content/export.xul
@@ -34,10 +34,6 @@
-
-
-
-
diff --git a/addon/chrome/locale/en-US/overlay.dtd b/addon/chrome/locale/en-US/overlay.dtd
index 918344e..4d79a4f 100644
--- a/addon/chrome/locale/en-US/overlay.dtd
+++ b/addon/chrome/locale/en-US/overlay.dtd
@@ -23,8 +23,7 @@
-
-
+
diff --git a/addon/chrome/locale/zh-CN/overlay.dtd b/addon/chrome/locale/zh-CN/overlay.dtd
index 0668ca0..fd91b77 100644
--- a/addon/chrome/locale/zh-CN/overlay.dtd
+++ b/addon/chrome/locale/zh-CN/overlay.dtd
@@ -23,8 +23,7 @@
-
-
+
diff --git a/build.js b/build.js
index aa80a7c..73cf96a 100644
--- a/build.js
+++ b/build.js
@@ -157,6 +157,11 @@ copyFileSync(
path.join(buildDir, "addon/components/zotero-protocol-handler.js")
);
+copyFileSync(
+ "src/Better Note Markdown.js",
+ path.join(buildDir, "addon/chrome/content/translators/Better Note Markdown.js")
+);
+
compressing.zip.compressDir(
path.join(buildDir, "addon"),
path.join(buildDir, `${name}.xpi`),
diff --git a/src/Better Note Markdown.js b/src/Better Note Markdown.js
new file mode 100644
index 0000000..2045a23
--- /dev/null
+++ b/src/Better Note Markdown.js
@@ -0,0 +1,1809 @@
+/*
+ ***** BEGIN LICENSE BLOCK *****
+
+ Copyright © 2021 Corporation for Digital Scholarship
+ Vienna, Virginia, USA
+ http://digitalscholar.org/
+
+ This file is part of Zotero.
+
+ Zotero is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Zotero is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with Zotero. If not, see .
+
+ ***** END LICENSE BLOCK *****
+*/
+
+let bundle;
+(function () {
+ function r(e, n, t) {
+ function o(i, f) {
+ if (!n[i]) {
+ if (!e[i]) {
+ var c = "function" == typeof require && require;
+ if (!f && c) return c(i, !0);
+ if (u) return u(i, !0);
+ var a = new Error("Cannot find module '" + i + "'");
+ throw ((a.code = "MODULE_NOT_FOUND"), a);
+ }
+ var p = (n[i] = { exports: {} });
+ e[i][0].call(
+ p.exports,
+ function (r) {
+ var n = e[i][1][r];
+ return o(n || r);
+ },
+ p,
+ p.exports,
+ r,
+ e,
+ n,
+ t
+ );
+ }
+ return n[i].exports;
+ }
+ for (
+ var u = "function" == typeof require && require, i = 0;
+ i < t.length;
+ i++
+ )
+ o(t[i]);
+ return o;
+ }
+ return r;
+})()(
+ {
+ 1: [
+ function (require, module, exports) {
+ // shim for using process in browser
+ var process = (module.exports = {});
+
+ // cached from whatever global is present so that test runners that stub it
+ // don't break things. But we need to wrap it in a try catch in case it is
+ // wrapped in strict mode code which doesn't define any globals. It's inside a
+ // function because try/catches deoptimize in certain engines.
+
+ var cachedSetTimeout;
+ var cachedClearTimeout;
+
+ function defaultSetTimout() {
+ throw new Error("setTimeout has not been defined");
+ }
+ function defaultClearTimeout() {
+ throw new Error("clearTimeout has not been defined");
+ }
+ (function () {
+ try {
+ if (typeof setTimeout === "function") {
+ cachedSetTimeout = setTimeout;
+ } else {
+ cachedSetTimeout = defaultSetTimout;
+ }
+ } catch (e) {
+ cachedSetTimeout = defaultSetTimout;
+ }
+ try {
+ if (typeof clearTimeout === "function") {
+ cachedClearTimeout = clearTimeout;
+ } else {
+ cachedClearTimeout = defaultClearTimeout;
+ }
+ } catch (e) {
+ cachedClearTimeout = defaultClearTimeout;
+ }
+ })();
+ function runTimeout(fun) {
+ if (cachedSetTimeout === setTimeout) {
+ //normal enviroments in sane situations
+ return setTimeout(fun, 0);
+ }
+ // if setTimeout wasn't available but was latter defined
+ if (
+ (cachedSetTimeout === defaultSetTimout || !cachedSetTimeout) &&
+ setTimeout
+ ) {
+ cachedSetTimeout = setTimeout;
+ return setTimeout(fun, 0);
+ }
+ try {
+ // when when somebody has screwed with setTimeout but no I.E. maddness
+ return cachedSetTimeout(fun, 0);
+ } catch (e) {
+ try {
+ // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally
+ return cachedSetTimeout.call(null, fun, 0);
+ } catch (e) {
+ // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error
+ return cachedSetTimeout.call(this, fun, 0);
+ }
+ }
+ }
+ function runClearTimeout(marker) {
+ if (cachedClearTimeout === clearTimeout) {
+ //normal enviroments in sane situations
+ return clearTimeout(marker);
+ }
+ // if clearTimeout wasn't available but was latter defined
+ if (
+ (cachedClearTimeout === defaultClearTimeout ||
+ !cachedClearTimeout) &&
+ clearTimeout
+ ) {
+ cachedClearTimeout = clearTimeout;
+ return clearTimeout(marker);
+ }
+ try {
+ // when when somebody has screwed with setTimeout but no I.E. maddness
+ return cachedClearTimeout(marker);
+ } catch (e) {
+ try {
+ // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally
+ return cachedClearTimeout.call(null, marker);
+ } catch (e) {
+ // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error.
+ // Some versions of I.E. have different rules for clearTimeout vs setTimeout
+ return cachedClearTimeout.call(this, marker);
+ }
+ }
+ }
+ var queue = [];
+ var draining = false;
+ var currentQueue;
+ var queueIndex = -1;
+
+ function cleanUpNextTick() {
+ if (!draining || !currentQueue) {
+ return;
+ }
+ draining = false;
+ if (currentQueue.length) {
+ queue = currentQueue.concat(queue);
+ } else {
+ queueIndex = -1;
+ }
+ if (queue.length) {
+ drainQueue();
+ }
+ }
+
+ function drainQueue() {
+ if (draining) {
+ return;
+ }
+ var timeout = runTimeout(cleanUpNextTick);
+ draining = true;
+
+ var len = queue.length;
+ while (len) {
+ currentQueue = queue;
+ queue = [];
+ while (++queueIndex < len) {
+ if (currentQueue) {
+ currentQueue[queueIndex].run();
+ }
+ }
+ queueIndex = -1;
+ len = queue.length;
+ }
+ currentQueue = null;
+ draining = false;
+ runClearTimeout(timeout);
+ }
+
+ process.nextTick = function (fun) {
+ var args = new Array(arguments.length - 1);
+ if (arguments.length > 1) {
+ for (var i = 1; i < arguments.length; i++) {
+ args[i - 1] = arguments[i];
+ }
+ }
+ queue.push(new Item(fun, args));
+ if (queue.length === 1 && !draining) {
+ runTimeout(drainQueue);
+ }
+ };
+
+ // v8 likes predictible objects
+ function Item(fun, array) {
+ this.fun = fun;
+ this.array = array;
+ }
+ Item.prototype.run = function () {
+ this.fun.apply(null, this.array);
+ };
+ process.title = "browser";
+ process.browser = true;
+ process.env = {};
+ process.argv = [];
+ process.version = ""; // empty string to avoid regexp issues
+ process.versions = {};
+
+ function noop() {}
+
+ process.on = noop;
+ process.addListener = noop;
+ process.once = noop;
+ process.off = noop;
+ process.removeListener = noop;
+ process.removeAllListeners = noop;
+ process.emit = noop;
+ process.prependListener = noop;
+ process.prependOnceListener = noop;
+
+ process.listeners = function (name) {
+ return [];
+ };
+
+ process.binding = function (name) {
+ throw new Error("process.binding is not supported");
+ };
+
+ process.cwd = function () {
+ return "/";
+ };
+ process.chdir = function (dir) {
+ throw new Error("process.chdir is not supported");
+ };
+ process.umask = function () {
+ return 0;
+ };
+ },
+ {},
+ ],
+ 2: [
+ function (require, module, exports) {
+ "use strict";
+
+ Object.defineProperty(exports, "__esModule", { value: true });
+
+ var highlightRegExp = /highlight-(?:text|source)-([a-z0-9]+)/;
+
+ function highlightedCodeBlock(turndownService) {
+ turndownService.addRule("highlightedCodeBlock", {
+ filter: function (node) {
+ var firstChild = node.firstChild;
+ return (
+ node.nodeName === "DIV" &&
+ highlightRegExp.test(node.className) &&
+ firstChild &&
+ firstChild.nodeName === "PRE"
+ );
+ },
+ replacement: function (content, node, options) {
+ var className = node.className || "";
+ var language = (className.match(highlightRegExp) || [
+ null,
+ "",
+ ])[1];
+
+ return (
+ "\n\n" +
+ options.fence +
+ language +
+ "\n" +
+ node.firstChild.textContent +
+ "\n" +
+ options.fence +
+ "\n\n"
+ );
+ },
+ });
+ }
+
+ function strikethrough(turndownService) {
+ turndownService.addRule("strikethrough", {
+ filter: ["del", "s", "strike"],
+ replacement: function (content) {
+ return "~" + content + "~";
+ },
+ });
+ }
+
+ var indexOf = Array.prototype.indexOf;
+ var every = Array.prototype.every;
+ var rules = {};
+
+ rules.tableCell = {
+ filter: ["th", "td"],
+ replacement: function (content, node) {
+ return cell(content, node);
+ },
+ };
+
+ rules.tableRow = {
+ filter: "tr",
+ replacement: function (content, node) {
+ var borderCells = "";
+ var alignMap = { left: ":--", right: "--:", center: ":-:" };
+
+ if (isHeadingRow(node)) {
+ for (var i = 0; i < node.childNodes.length; i++) {
+ var border = "---";
+ var align = (
+ node.childNodes[i].getAttribute("align") || ""
+ ).toLowerCase();
+
+ if (align) border = alignMap[align] || border;
+
+ borderCells += cell(border, node.childNodes[i]);
+ }
+ }
+ return "\n" + content + (borderCells ? "\n" + borderCells : "");
+ },
+ };
+
+ rules.table = {
+ // Only convert tables with a heading row.
+ // Tables with no heading row are kept using `keep` (see below).
+ filter: function (node) {
+ return node.nodeName === "TABLE" && isHeadingRow(node.rows[0]);
+ },
+
+ replacement: function (content) {
+ // Ensure there are no blank lines
+ content = content.replace("\n\n", "\n");
+ return "\n\n" + content + "\n\n";
+ },
+ };
+
+ rules.tableSection = {
+ filter: ["thead", "tbody", "tfoot"],
+ replacement: function (content) {
+ return content;
+ },
+ };
+
+ // A tr is a heading row if:
+ // - the parent is a THEAD
+ // - or if its the first child of the TABLE or the first TBODY (possibly
+ // following a blank THEAD)
+ // - and every cell is a TH
+ function isHeadingRow(tr) {
+ var parentNode = tr.parentNode;
+ return (
+ parentNode.nodeName === "THEAD" ||
+ (parentNode.firstChild === tr &&
+ (parentNode.nodeName === "TABLE" || isFirstTbody(parentNode)) &&
+ every.call(tr.childNodes, function (n) {
+ return n.nodeName === "TH";
+ }))
+ );
+ }
+
+ function isFirstTbody(element) {
+ var previousSibling = element.previousSibling;
+ return (
+ element.nodeName === "TBODY" &&
+ (!previousSibling ||
+ (previousSibling.nodeName === "THEAD" &&
+ /^\s*$/i.test(previousSibling.textContent)))
+ );
+ }
+
+ function cell(content, node) {
+ var index = indexOf.call(node.parentNode.childNodes, node);
+ var prefix = " ";
+ if (index === 0) prefix = "| ";
+ return prefix + content + " |";
+ }
+
+ function tables(turndownService) {
+ turndownService.keep(function (node) {
+ return node.nodeName === "TABLE" && !isHeadingRow(node.rows[0]);
+ });
+ for (var key in rules) turndownService.addRule(key, rules[key]);
+ }
+
+ function taskListItems(turndownService) {
+ turndownService.addRule("taskListItems", {
+ filter: function (node) {
+ return (
+ node.type === "checkbox" && node.parentNode.nodeName === "LI"
+ );
+ },
+ replacement: function (content, node) {
+ return (node.checked ? "[x]" : "[ ]") + " ";
+ },
+ });
+ }
+
+ function gfm(turndownService) {
+ turndownService.use([
+ highlightedCodeBlock,
+ strikethrough,
+ tables,
+ taskListItems,
+ ]);
+ }
+
+ exports.gfm = gfm;
+ exports.highlightedCodeBlock = highlightedCodeBlock;
+ exports.strikethrough = strikethrough;
+ exports.tables = tables;
+ exports.taskListItems = taskListItems;
+ },
+ {},
+ ],
+ 3: [
+ function (require, module, exports) {
+ (function (process) {
+ (function () {
+ (function (global, factory) {
+ typeof exports === "object" && typeof module !== "undefined"
+ ? (module.exports = factory())
+ : typeof define === "function" && define.amd
+ ? define(factory)
+ : ((global =
+ typeof globalThis !== "undefined"
+ ? globalThis
+ : global || self),
+ (global.TurndownService = factory()));
+ })(this, function () {
+ "use strict";
+
+ function extend(destination) {
+ for (var i = 1; i < arguments.length; i++) {
+ var source = arguments[i];
+ for (var key in source) {
+ if (source.hasOwnProperty(key))
+ destination[key] = source[key];
+ }
+ }
+ return destination;
+ }
+
+ function repeat(character, count) {
+ return Array(count + 1).join(character);
+ }
+
+ function trimLeadingNewlines(string) {
+ return string.replace(/^\n*/, "");
+ }
+
+ function trimTrailingNewlines(string) {
+ // avoid match-at-end regexp bottleneck, see #370
+ var indexEnd = string.length;
+ while (indexEnd > 0 && string[indexEnd - 1] === "\n")
+ indexEnd--;
+ return string.substring(0, indexEnd);
+ }
+
+ var blockElements = [
+ "ADDRESS",
+ "ARTICLE",
+ "ASIDE",
+ "AUDIO",
+ "BLOCKQUOTE",
+ "BODY",
+ "CANVAS",
+ "CENTER",
+ "DD",
+ "DIR",
+ "DIV",
+ "DL",
+ "DT",
+ "FIELDSET",
+ "FIGCAPTION",
+ "FIGURE",
+ "FOOTER",
+ "FORM",
+ "FRAMESET",
+ "H1",
+ "H2",
+ "H3",
+ "H4",
+ "H5",
+ "H6",
+ "HEADER",
+ "HGROUP",
+ "HR",
+ "HTML",
+ "ISINDEX",
+ "LI",
+ "MAIN",
+ "MENU",
+ "NAV",
+ "NOFRAMES",
+ "NOSCRIPT",
+ "OL",
+ "OUTPUT",
+ "P",
+ "PRE",
+ "SECTION",
+ "TABLE",
+ "TBODY",
+ "TD",
+ "TFOOT",
+ "TH",
+ "THEAD",
+ "TR",
+ "UL",
+ ];
+
+ function isBlock(node) {
+ return is(node, blockElements);
+ }
+
+ var voidElements = [
+ "AREA",
+ "BASE",
+ "BR",
+ "COL",
+ "COMMAND",
+ "EMBED",
+ "HR",
+ "IMG",
+ "INPUT",
+ "KEYGEN",
+ "LINK",
+ "META",
+ "PARAM",
+ "SOURCE",
+ "TRACK",
+ "WBR",
+ ];
+
+ function isVoid(node) {
+ return is(node, voidElements);
+ }
+
+ function hasVoid(node) {
+ return has(node, voidElements);
+ }
+
+ var meaningfulWhenBlankElements = [
+ "A",
+ "TABLE",
+ "THEAD",
+ "TBODY",
+ "TFOOT",
+ "TH",
+ "TD",
+ "IFRAME",
+ "SCRIPT",
+ "AUDIO",
+ "VIDEO",
+ ];
+
+ function isMeaningfulWhenBlank(node) {
+ return is(node, meaningfulWhenBlankElements);
+ }
+
+ function hasMeaningfulWhenBlank(node) {
+ return has(node, meaningfulWhenBlankElements);
+ }
+
+ function is(node, tagNames) {
+ return tagNames.indexOf(node.nodeName) >= 0;
+ }
+
+ function has(node, tagNames) {
+ return (
+ node.getElementsByTagName &&
+ tagNames.some(function (tagName) {
+ return node.getElementsByTagName(tagName).length;
+ })
+ );
+ }
+
+ var rules = {};
+
+ rules.paragraph = {
+ filter: "p",
+
+ replacement: function (content) {
+ return "\n\n" + content + "\n\n";
+ },
+ };
+
+ rules.lineBreak = {
+ filter: "br",
+
+ replacement: function (content, node, options) {
+ return options.br + "\n";
+ },
+ };
+
+ rules.heading = {
+ filter: ["h1", "h2", "h3", "h4", "h5", "h6"],
+
+ replacement: function (content, node, options) {
+ var hLevel = Number(node.nodeName.charAt(1));
+
+ if (options.headingStyle === "setext" && hLevel < 3) {
+ var underline = repeat(
+ hLevel === 1 ? "=" : "-",
+ content.length
+ );
+ return "\n\n" + content + "\n" + underline + "\n\n";
+ } else {
+ return (
+ "\n\n" + repeat("#", hLevel) + " " + content + "\n\n"
+ );
+ }
+ },
+ };
+
+ rules.blockquote = {
+ filter: "blockquote",
+
+ replacement: function (content) {
+ content = content.replace(/^\n+|\n+$/g, "");
+ content = content.replace(/^/gm, "> ");
+ return "\n\n" + content + "\n\n";
+ },
+ };
+
+ rules.list = {
+ filter: ["ul", "ol"],
+
+ replacement: function (content, node) {
+ var parent = node.parentNode;
+ if (
+ parent.nodeName === "LI" &&
+ parent.lastElementChild === node
+ ) {
+ return "\n" + content;
+ } else {
+ return "\n\n" + content + "\n\n";
+ }
+ },
+ };
+
+ rules.listItem = {
+ filter: "li",
+
+ replacement: function (content, node, options) {
+ content = content
+ .replace(/^\n+/, "") // remove leading newlines
+ .replace(/\n+$/, "\n") // replace trailing newlines with just a single one
+ .replace(/\n/gm, "\n "); // indent
+ var prefix = options.bulletListMarker + " ";
+ var parent = node.parentNode;
+ if (parent.nodeName === "OL") {
+ var start = parent.getAttribute("start");
+ var index = Array.prototype.indexOf.call(
+ parent.children,
+ node
+ );
+ prefix =
+ (start ? Number(start) + index : index + 1) + ". ";
+ }
+ return (
+ prefix +
+ content +
+ (node.nextSibling && !/\n$/.test(content) ? "\n" : "")
+ );
+ },
+ };
+
+ rules.indentedCodeBlock = {
+ filter: function (node, options) {
+ return (
+ options.codeBlockStyle === "indented" &&
+ node.nodeName === "PRE" &&
+ node.firstChild &&
+ node.firstChild.nodeName === "CODE"
+ );
+ },
+
+ replacement: function (content, node, options) {
+ return (
+ "\n\n " +
+ node.firstChild.textContent.replace(/\n/g, "\n ") +
+ "\n\n"
+ );
+ },
+ };
+
+ rules.fencedCodeBlock = {
+ filter: function (node, options) {
+ return (
+ options.codeBlockStyle === "fenced" &&
+ node.nodeName === "PRE" &&
+ node.firstChild &&
+ node.firstChild.nodeName === "CODE"
+ );
+ },
+
+ replacement: function (content, node, options) {
+ var className = node.firstChild.getAttribute("class") || "";
+ var language = (className.match(/language-(\S+)/) || [
+ null,
+ "",
+ ])[1];
+ var code = node.firstChild.textContent;
+
+ var fenceChar = options.fence.charAt(0);
+ var fenceSize = 3;
+ var fenceInCodeRegex = new RegExp(
+ "^" + fenceChar + "{3,}",
+ "gm"
+ );
+
+ var match;
+ while ((match = fenceInCodeRegex.exec(code))) {
+ if (match[0].length >= fenceSize) {
+ fenceSize = match[0].length + 1;
+ }
+ }
+
+ var fence = repeat(fenceChar, fenceSize);
+
+ return (
+ "\n\n" +
+ fence +
+ language +
+ "\n" +
+ code.replace(/\n$/, "") +
+ "\n" +
+ fence +
+ "\n\n"
+ );
+ },
+ };
+
+ rules.horizontalRule = {
+ filter: "hr",
+
+ replacement: function (content, node, options) {
+ return "\n\n" + options.hr + "\n\n";
+ },
+ };
+
+ rules.inlineLink = {
+ filter: function (node, options) {
+ return (
+ options.linkStyle === "inlined" &&
+ node.nodeName === "A" &&
+ node.getAttribute("href")
+ );
+ },
+
+ replacement: function (content, node) {
+ var href = node.getAttribute("href");
+ var title = cleanAttribute(node.getAttribute("title"));
+ if (title) title = ' "' + title + '"';
+ return "[" + content + "](" + href + title + ")";
+ },
+ };
+
+ rules.referenceLink = {
+ filter: function (node, options) {
+ return (
+ options.linkStyle === "referenced" &&
+ node.nodeName === "A" &&
+ node.getAttribute("href")
+ );
+ },
+
+ replacement: function (content, node, options) {
+ var href = node.getAttribute("href");
+ var title = cleanAttribute(node.getAttribute("title"));
+ if (title) title = ' "' + title + '"';
+ var replacement;
+ var reference;
+
+ switch (options.linkReferenceStyle) {
+ case "collapsed":
+ replacement = "[" + content + "][]";
+ reference = "[" + content + "]: " + href + title;
+ break;
+ case "shortcut":
+ replacement = "[" + content + "]";
+ reference = "[" + content + "]: " + href + title;
+ break;
+ default:
+ var id = this.references.length + 1;
+ replacement = "[" + content + "][" + id + "]";
+ reference = "[" + id + "]: " + href + title;
+ }
+
+ this.references.push(reference);
+ return replacement;
+ },
+
+ references: [],
+
+ append: function (options) {
+ var references = "";
+ if (this.references.length) {
+ references = "\n\n" + this.references.join("\n") + "\n\n";
+ this.references = []; // Reset references
+ }
+ return references;
+ },
+ };
+
+ rules.emphasis = {
+ filter: ["em", "i"],
+
+ replacement: function (content, node, options) {
+ if (!content.trim()) return "";
+ return options.emDelimiter + content + options.emDelimiter;
+ },
+ };
+
+ rules.strong = {
+ filter: ["strong", "b"],
+
+ replacement: function (content, node, options) {
+ if (!content.trim()) return "";
+ return (
+ options.strongDelimiter + content + options.strongDelimiter
+ );
+ },
+ };
+
+ rules.code = {
+ filter: function (node) {
+ var hasSiblings = node.previousSibling || node.nextSibling;
+ var isCodeBlock =
+ node.parentNode.nodeName === "PRE" && !hasSiblings;
+
+ return node.nodeName === "CODE" && !isCodeBlock;
+ },
+
+ replacement: function (content) {
+ if (!content) return "";
+ content = content.replace(/\r?\n|\r/g, " ");
+
+ var extraSpace = /^`|^ .*?[^ ].* $|`$/.test(content)
+ ? " "
+ : "";
+ var delimiter = "`";
+ var matches = content.match(/`+/gm) || [];
+ while (matches.indexOf(delimiter) !== -1)
+ delimiter = delimiter + "`";
+
+ return (
+ delimiter + extraSpace + content + extraSpace + delimiter
+ );
+ },
+ };
+
+ rules.image = {
+ filter: "img",
+
+ replacement: function (content, node) {
+ var alt = cleanAttribute(node.getAttribute("alt"));
+ var src = node.getAttribute("src") || "";
+ var title = cleanAttribute(node.getAttribute("title"));
+ var titlePart = title ? ' "' + title + '"' : "";
+ return src
+ ? "![" + alt + "]" + "(" + src + titlePart + ")"
+ : "";
+ },
+ };
+
+ function cleanAttribute(attribute) {
+ return attribute ? attribute.replace(/(\n+\s*)+/g, "\n") : "";
+ }
+
+ /**
+ * Manages a collection of rules used to convert HTML to Markdown
+ */
+
+ function Rules(options) {
+ this.options = options;
+ this._keep = [];
+ this._remove = [];
+
+ this.blankRule = {
+ replacement: options.blankReplacement,
+ };
+
+ this.keepReplacement = options.keepReplacement;
+
+ this.defaultRule = {
+ replacement: options.defaultReplacement,
+ };
+
+ this.array = [];
+ for (var key in options.rules)
+ this.array.push(options.rules[key]);
+ }
+
+ Rules.prototype = {
+ add: function (key, rule) {
+ this.array.unshift(rule);
+ },
+
+ keep: function (filter) {
+ this._keep.unshift({
+ filter: filter,
+ replacement: this.keepReplacement,
+ });
+ },
+
+ remove: function (filter) {
+ this._remove.unshift({
+ filter: filter,
+ replacement: function () {
+ return "";
+ },
+ });
+ },
+
+ forNode: function (node) {
+ if (node.isBlank) return this.blankRule;
+ var rule;
+
+ if ((rule = findRule(this.array, node, this.options)))
+ return rule;
+ if ((rule = findRule(this._keep, node, this.options)))
+ return rule;
+ if ((rule = findRule(this._remove, node, this.options)))
+ return rule;
+
+ return this.defaultRule;
+ },
+
+ forEach: function (fn) {
+ for (var i = 0; i < this.array.length; i++)
+ fn(this.array[i], i);
+ },
+ };
+
+ function findRule(rules, node, options) {
+ for (var i = 0; i < rules.length; i++) {
+ var rule = rules[i];
+ if (filterValue(rule, node, options)) return rule;
+ }
+ return void 0;
+ }
+
+ function filterValue(rule, node, options) {
+ var filter = rule.filter;
+ if (typeof filter === "string") {
+ if (filter === node.nodeName.toLowerCase()) return true;
+ } else if (Array.isArray(filter)) {
+ if (filter.indexOf(node.nodeName.toLowerCase()) > -1)
+ return true;
+ } else if (typeof filter === "function") {
+ if (filter.call(rule, node, options)) return true;
+ } else {
+ throw new TypeError(
+ "`filter` needs to be a string, array, or function"
+ );
+ }
+ }
+
+ /**
+ * The collapseWhitespace function is adapted from collapse-whitespace
+ * by Luc Thevenard.
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2014 Luc Thevenard
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+ /**
+ * collapseWhitespace(options) removes extraneous whitespace from an the given element.
+ *
+ * @param {Object} options
+ */
+ function collapseWhitespace(options) {
+ var element = options.element;
+ var isBlock = options.isBlock;
+ var isVoid = options.isVoid;
+ var isPre =
+ options.isPre ||
+ function (node) {
+ return node.nodeName === "PRE";
+ };
+
+ if (!element.firstChild || isPre(element)) return;
+
+ var prevText = null;
+ var keepLeadingWs = false;
+
+ var prev = null;
+ var node = next(prev, element, isPre);
+
+ while (node !== element) {
+ if (node.nodeType === 3 || node.nodeType === 4) {
+ // Node.TEXT_NODE or Node.CDATA_SECTION_NODE
+ var text = node.data.replace(/[ \r\n\t]+/g, " ");
+
+ if (
+ (!prevText || / $/.test(prevText.data)) &&
+ !keepLeadingWs &&
+ text[0] === " "
+ ) {
+ text = text.substr(1);
+ }
+
+ // `text` might be empty at this point.
+ if (!text) {
+ node = remove(node);
+ continue;
+ }
+
+ node.data = text;
+
+ prevText = node;
+ } else if (node.nodeType === 1) {
+ // Node.ELEMENT_NODE
+ if (isBlock(node) || node.nodeName === "BR") {
+ if (prevText) {
+ prevText.data = prevText.data.replace(/ $/, "");
+ }
+
+ prevText = null;
+ keepLeadingWs = false;
+ } else if (isVoid(node) || isPre(node)) {
+ // Avoid trimming space around non-block, non-BR void elements and inline PRE.
+ prevText = null;
+ keepLeadingWs = true;
+ } else if (prevText) {
+ // Drop protection if set previously.
+ keepLeadingWs = false;
+ }
+ } else {
+ node = remove(node);
+ continue;
+ }
+
+ var nextNode = next(prev, node, isPre);
+ prev = node;
+ node = nextNode;
+ }
+
+ if (prevText) {
+ prevText.data = prevText.data.replace(/ $/, "");
+ if (!prevText.data) {
+ remove(prevText);
+ }
+ }
+ }
+
+ /**
+ * remove(node) removes the given node from the DOM and returns the
+ * next node in the sequence.
+ *
+ * @param {Node} node
+ * @return {Node} node
+ */
+ function remove(node) {
+ var next = node.nextSibling || node.parentNode;
+
+ node.parentNode.removeChild(node);
+
+ return next;
+ }
+
+ /**
+ * next(prev, current, isPre) returns the next node in the sequence, given the
+ * current and previous nodes.
+ *
+ * @param {Node} prev
+ * @param {Node} current
+ * @param {Function} isPre
+ * @return {Node}
+ */
+ function next(prev, current, isPre) {
+ if ((prev && prev.parentNode === current) || isPre(current)) {
+ return current.nextSibling || current.parentNode;
+ }
+
+ return (
+ current.firstChild ||
+ current.nextSibling ||
+ current.parentNode
+ );
+ }
+
+ /*
+ * Set up window for Node.js
+ */
+
+ var root = typeof window !== "undefined" ? window : {};
+
+ /*
+ * Parsing HTML strings
+ */
+
+ function canParseHTMLNatively() {
+ var Parser = root.DOMParser;
+ var canParse = false;
+
+ // Adapted from https://gist.github.com/1129031
+ // Firefox/Opera/IE throw errors on unsupported types
+ try {
+ // WebKit returns null on unsupported types
+ if (new Parser().parseFromString("", "text/html")) {
+ canParse = true;
+ }
+ } catch (e) {}
+
+ return canParse;
+ }
+
+ function createHTMLParser() {
+ var Parser = function () {};
+
+ {
+ if (shouldUseActiveX()) {
+ Parser.prototype.parseFromString = function (string) {
+ var doc = new window.ActiveXObject("htmlfile");
+ doc.designMode = "on"; // disable on-page scripts
+ doc.open();
+ doc.write(string);
+ doc.close();
+ return doc;
+ };
+ } else {
+ Parser.prototype.parseFromString = function (string) {
+ var doc = document.implementation.createHTMLDocument("");
+ doc.open();
+ doc.write(string);
+ doc.close();
+ return doc;
+ };
+ }
+ }
+ return Parser;
+ }
+
+ function shouldUseActiveX() {
+ var useActiveX = false;
+ try {
+ document.implementation.createHTMLDocument("").open();
+ } catch (e) {
+ if (window.ActiveXObject) useActiveX = true;
+ }
+ return useActiveX;
+ }
+
+ function RootNode(input, options) {
+ var root;
+ if (typeof input === "string") {
+ var doc = htmlParser().parseFromString(
+ // DOM parsers arrange elements in the and .
+ // Wrapping in a custom element ensures elements are reliably arranged in
+ // a single element.
+ '' + input + "",
+ "text/html"
+ );
+ root = doc.getElementById("turndown-root");
+ } else {
+ root = input.cloneNode(true);
+ }
+ collapseWhitespace({
+ element: root,
+ isBlock: isBlock,
+ isVoid: isVoid,
+ isPre: options.preformattedCode ? isPreOrCode : null,
+ });
+
+ return root;
+ }
+
+ var _htmlParser;
+ function htmlParser() {
+ _htmlParser = _htmlParser || new HTMLParser();
+ return _htmlParser;
+ }
+
+ function isPreOrCode(node) {
+ return node.nodeName === "PRE" || node.nodeName === "CODE";
+ }
+
+ function Node(node, options) {
+ node.isBlock = isBlock(node);
+ node.isCode =
+ node.nodeName === "CODE" || node.parentNode.isCode;
+ node.isBlank = isBlank(node);
+ node.flankingWhitespace = flankingWhitespace(node, options);
+ return node;
+ }
+
+ function isBlank(node) {
+ return (
+ !isVoid(node) &&
+ !isMeaningfulWhenBlank(node) &&
+ /^\s*$/i.test(node.textContent) &&
+ !hasVoid(node) &&
+ !hasMeaningfulWhenBlank(node)
+ );
+ }
+
+ function flankingWhitespace(node, options) {
+ if (node.isBlock || (options.preformattedCode && node.isCode)) {
+ return { leading: "", trailing: "" };
+ }
+
+ var edges = edgeWhitespace(node.textContent);
+
+ // abandon leading ASCII WS if left-flanked by ASCII WS
+ if (
+ edges.leadingAscii &&
+ isFlankedByWhitespace("left", node, options)
+ ) {
+ edges.leading = edges.leadingNonAscii;
+ }
+
+ // abandon trailing ASCII WS if right-flanked by ASCII WS
+ if (
+ edges.trailingAscii &&
+ isFlankedByWhitespace("right", node, options)
+ ) {
+ edges.trailing = edges.trailingNonAscii;
+ }
+
+ return { leading: edges.leading, trailing: edges.trailing };
+ }
+
+ function edgeWhitespace(string) {
+ var m = string.match(
+ /^(([ \t\r\n]*)(\s*))[\s\S]*?((\s*?)([ \t\r\n]*))$/
+ );
+ return {
+ leading: m[1], // whole string for whitespace-only strings
+ leadingAscii: m[2],
+ leadingNonAscii: m[3],
+ trailing: m[4], // empty for whitespace-only strings
+ trailingNonAscii: m[5],
+ trailingAscii: m[6],
+ };
+ }
+
+ function isFlankedByWhitespace(side, node, options) {
+ var sibling;
+ var regExp;
+ var isFlanked;
+
+ if (side === "left") {
+ sibling = node.previousSibling;
+ regExp = / $/;
+ } else {
+ sibling = node.nextSibling;
+ regExp = /^ /;
+ }
+
+ if (sibling) {
+ if (sibling.nodeType === 3) {
+ isFlanked = regExp.test(sibling.nodeValue);
+ } else if (
+ options.preformattedCode &&
+ sibling.nodeName === "CODE"
+ ) {
+ isFlanked = false;
+ } else if (sibling.nodeType === 1 && !isBlock(sibling)) {
+ isFlanked = regExp.test(sibling.textContent);
+ }
+ }
+ return isFlanked;
+ }
+
+ var reduce = Array.prototype.reduce;
+ var escapes = [
+ // [/\\/g, '\\\\'],
+ // [/\*/g, '\\*'],
+ [/^-/g, "\\-"],
+ [/^\+ /g, "\\+ "],
+ [/^(=+)/g, "\\$1"],
+ [/^(#{1,6}) /g, "\\$1 "],
+ [/`/g, "\\`"],
+ [/^~~~/g, "\\~~~"],
+ [/\[/g, "\\["],
+ [/\]/g, "\\]"],
+ [/^>/g, "\\>"],
+ [/_/g, "\\_"],
+ [/^(\d+)\. /g, "$1\\. "],
+ ];
+
+ function TurndownService(options) {
+ if (!(this instanceof TurndownService))
+ return new TurndownService(options);
+
+ var defaults = {
+ rules: rules,
+ headingStyle: "setext",
+ hr: "* * *",
+ bulletListMarker: "*",
+ codeBlockStyle: "indented",
+ fence: "```",
+ emDelimiter: "_",
+ strongDelimiter: "**",
+ linkStyle: "inlined",
+ linkReferenceStyle: "full",
+ br: " ",
+ preformattedCode: false,
+ blankReplacement: function (content, node) {
+ return node.isBlock ? "\n\n" : "";
+ },
+ keepReplacement: function (content, node) {
+ return node.isBlock
+ ? "\n\n" + node.outerHTML + "\n\n"
+ : node.outerHTML;
+ },
+ defaultReplacement: function (content, node) {
+ return node.isBlock ? "\n\n" + content + "\n\n" : content;
+ },
+ };
+ this.options = extend({}, defaults, options);
+ this.rules = new Rules(this.options);
+ }
+
+ TurndownService.prototype = {
+ /**
+ * The entry point for converting a string or DOM node to Markdown
+ * @public
+ * @param {String|HTMLElement} input The string or DOM node to convert
+ * @returns A Markdown representation of the input
+ * @type String
+ */
+
+ turndown: function (input) {
+ if (!canConvert(input)) {
+ throw new TypeError(
+ input +
+ " is not a string, or an element/document/fragment node."
+ );
+ }
+
+ if (input === "") return "";
+
+ var output = process.call(
+ this,
+ new RootNode(input, this.options)
+ );
+ return postProcess.call(this, output);
+ },
+
+ /**
+ * Add one or more plugins
+ * @public
+ * @param {Function|Array} plugin The plugin or array of plugins to add
+ * @returns The Turndown instance for chaining
+ * @type Object
+ */
+
+ use: function (plugin) {
+ if (Array.isArray(plugin)) {
+ for (var i = 0; i < plugin.length; i++) this.use(plugin[i]);
+ } else if (typeof plugin === "function") {
+ plugin(this);
+ } else {
+ throw new TypeError(
+ "plugin must be a Function or an Array of Functions"
+ );
+ }
+ return this;
+ },
+
+ /**
+ * Adds a rule
+ * @public
+ * @param {String} key The unique key of the rule
+ * @param {Object} rule The rule
+ * @returns The Turndown instance for chaining
+ * @type Object
+ */
+
+ addRule: function (key, rule) {
+ this.rules.add(key, rule);
+ return this;
+ },
+
+ /**
+ * Keep a node (as HTML) that matches the filter
+ * @public
+ * @param {String|Array|Function} filter The unique key of the rule
+ * @returns The Turndown instance for chaining
+ * @type Object
+ */
+
+ keep: function (filter) {
+ this.rules.keep(filter);
+ return this;
+ },
+
+ /**
+ * Remove a node that matches the filter
+ * @public
+ * @param {String|Array|Function} filter The unique key of the rule
+ * @returns The Turndown instance for chaining
+ * @type Object
+ */
+
+ remove: function (filter) {
+ this.rules.remove(filter);
+ return this;
+ },
+
+ /**
+ * Escapes Markdown syntax
+ * @public
+ * @param {String} string The string to escape
+ * @returns A string with Markdown syntax escaped
+ * @type String
+ */
+
+ escape: function (string) {
+ return escapes.reduce(function (accumulator, escape) {
+ return accumulator.replace(escape[0], escape[1]);
+ }, string);
+ },
+ };
+
+ /**
+ * Reduces a DOM node down to its Markdown string equivalent
+ * @private
+ * @param {HTMLElement} parentNode The node to convert
+ * @returns A Markdown representation of the node
+ * @type String
+ */
+
+ function process(parentNode) {
+ var self = this;
+ return reduce.call(
+ parentNode.childNodes,
+ function (output, node) {
+ node = new Node(node, self.options);
+
+ var replacement = "";
+ if (node.nodeType === 3) {
+ replacement = node.isCode
+ ? node.nodeValue
+ : self.escape(node.nodeValue);
+ } else if (node.nodeType === 1) {
+ replacement = replacementForNode.call(self, node);
+ }
+
+ return join(output, replacement);
+ },
+ ""
+ );
+ }
+
+ /**
+ * Appends strings as each rule requires and trims the output
+ * @private
+ * @param {String} output The conversion output
+ * @returns A trimmed version of the ouput
+ * @type String
+ */
+
+ function postProcess(output) {
+ var self = this;
+ this.rules.forEach(function (rule) {
+ if (typeof rule.append === "function") {
+ output = join(output, rule.append(self.options));
+ }
+ });
+
+ return output
+ .replace(/^[\t\r\n]+/, "")
+ .replace(/[\t\r\n\s]+$/, "");
+ }
+
+ /**
+ * Converts an element node to its Markdown equivalent
+ * @private
+ * @param {HTMLElement} node The node to convert
+ * @returns A Markdown representation of the node
+ * @type String
+ */
+
+ function replacementForNode(node) {
+ var rule = this.rules.forNode(node);
+ var content = process.call(this, node);
+ var whitespace = node.flankingWhitespace;
+ if (whitespace.leading || whitespace.trailing)
+ content = content.trim();
+ return (
+ whitespace.leading +
+ rule.replacement(content, node, this.options) +
+ whitespace.trailing
+ );
+ }
+
+ /**
+ * Joins replacement to the current output with appropriate number of new lines
+ * @private
+ * @param {String} output The current conversion output
+ * @param {String} replacement The string to append to the output
+ * @returns Joined output
+ * @type String
+ */
+
+ function join(output, replacement) {
+ var s1 = trimTrailingNewlines(output);
+ var s2 = trimLeadingNewlines(replacement);
+ var nls = Math.max(
+ output.length - s1.length,
+ replacement.length - s2.length
+ );
+ var separator = "\n\n".substring(0, nls);
+
+ return s1 + separator + s2;
+ }
+
+ /**
+ * Determines whether an input can be converted
+ * @private
+ * @param {String|HTMLElement} input Describe this parameter
+ * @returns Describe what it returns
+ * @type String|Object|Array|Boolean|Number
+ */
+
+ function canConvert(input) {
+ return (
+ input != null &&
+ (typeof input === "string" ||
+ (input.nodeType &&
+ (input.nodeType === 1 ||
+ input.nodeType === 9 ||
+ input.nodeType === 11)))
+ );
+ }
+
+ return TurndownService;
+ });
+ }.call(this));
+ }.call(this, require("_process")));
+ },
+ { _process: 1 },
+ ],
+ 4: [
+ function (require, module, exports) {
+ /*
+ ***** BEGIN LICENSE BLOCK *****
+
+ Copyright © 2021 Corporation for Digital Scholarship
+ Vienna, Virginia, USA
+ http://digitalscholar.org/
+
+ This file is part of Zotero.
+
+ Zotero is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Zotero is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with Zotero. If not, see .
+
+ ***** END LICENSE BLOCK *****
+*/
+
+ let TurndownService = require("turndown/lib/turndown.browser.umd");
+ let turndownPluginGfm = require("turndown-plugin-gfm");
+
+ let turndownService = new TurndownService({
+ headingStyle: "atx",
+ bulletListMarker: "-",
+ emDelimiter: "*",
+ codeBlockStyle: "fenced",
+ });
+
+ turndownService.use(turndownPluginGfm.gfm);
+
+ async function convert(_Zotero, doc) {
+ // Transform `style="text-decoration: line-through"` nodes to (TinyMCE doesn't support )
+ doc.querySelectorAll("span").forEach(function (span) {
+ if (span.style.textDecoration === "line-through") {
+ let s = doc.createElement("s");
+ s.append(...span.childNodes);
+ span.replaceWith(s);
+ }
+ });
+
+ // Turndown wants pre content inside additional code block
+ doc.querySelectorAll("pre").forEach(function (pre) {
+ let code = doc.createElement("code");
+ code.append(...pre.childNodes);
+ pre.append(code);
+ });
+
+ // Insert a PDF link for highlight and image annotation nodes
+ doc
+ .querySelectorAll('span[class="highlight"], img[data-annotation]')
+ .forEach(function (node) {
+ try {
+ var annotation = JSON.parse(
+ decodeURIComponent(node.getAttribute("data-annotation"))
+ );
+ } catch (e) {}
+
+ if (annotation) {
+ // annotation.uri was used before note-editor v4
+ let uri = annotation.attachmentURI || annotation.uri;
+ let position = annotation.position;
+ if (
+ Zotero.getOption("includeAppLinks") &&
+ typeof uri === "string" &&
+ typeof position === "object"
+ ) {
+ let openURI;
+ let uriParts = uri.split("/");
+ let libraryType = uriParts[3];
+ let key = uriParts[6];
+ 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 a = doc.createElement("a");
+ a.href = openURI;
+ a.append("pdf");
+ let fragment = doc.createDocumentFragment();
+ fragment.append(" (", a, ") ");
+
+ let nextNode = node.nextElementSibling;
+ if (nextNode && nextNode.classList.contains("citation")) {
+ nextNode.parentNode.insertBefore(
+ fragment,
+ nextNode.nextSibling
+ );
+ } else {
+ node.parentNode.insertBefore(fragment, node.nextSibling);
+ }
+ }
+ }
+ });
+
+ for (img of doc.querySelectorAll("img[data-attachment-key]")) {
+ let imgKey = img.getAttribute("data-attachment-key");
+ const attachmentItem = await _Zotero.Items.getByLibraryAndKeyAsync(
+ _Zotero.Knowledge4Zotero.knowledge._exportNote.libraryID,
+ imgKey
+ );
+ Zotero.debug(attachmentItem);
+
+ let oldFile = String(await attachmentItem.getFilePathAsync());
+ Zotero.debug(oldFile);
+ let ext = oldFile.split(".").pop();
+ let newFile = _Zotero.File.copyToUnique(
+ oldFile,
+ `${_Zotero.Knowledge4Zotero.knowledge._exportPath}\\${imgKey}.${ext}`
+ );
+ Zotero.debug(newFile.path);
+
+ img.setAttribute(
+ "src",
+ _Zotero.isMac ? "file://" + newFile.path : newFile.path
+ );
+ img.setAttribute("alt", "image");
+ }
+
+ // Transform citations to links
+ doc
+ .querySelectorAll('span[class="citation"]')
+ .forEach(function (span) {
+ try {
+ var citation = JSON.parse(
+ decodeURIComponent(span.getAttribute("data-citation"))
+ );
+ } catch (e) {}
+
+ if (
+ citation &&
+ citation.citationItems &&
+ citation.citationItems.length
+ ) {
+ 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[6];
+ 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 items = Array.from(
+ span.querySelectorAll(".citation-item")
+ ).map((x) => x.textContent);
+ // 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 (!items.length) {
+ items = span.textContent.slice(1, -1).split("; ");
+ }
+
+ span.innerHTML =
+ "(" +
+ items
+ .map((item, i) => {
+ return Zotero.getOption("includeAppLinks")
+ ? `${item}`
+ : item;
+ })
+ .join("; ") +
+ ")";
+ }
+ });
+
+ return turndownService.turndown(doc.body);
+ }
+
+ bundle = { convert };
+ },
+ { "turndown-plugin-gfm": 2, "turndown/lib/turndown.browser.umd": 3 },
+ ],
+ },
+ {},
+ [4]
+);
+
+async function doExport() {
+ const _Zotero = Components.classes["@zotero.org/Zotero;1"].getService(
+ Components.interfaces.nsISupports
+ ).wrappedJSObject;
+ Zotero.setCharacterSet("utf-8");
+ var item;
+ let first = true;
+ while ((item = Zotero.nextItem())) {
+ if (item.itemType === "note" || item.itemType === "attachment") {
+ let doc = new DOMParser().parseFromString(item.note || "", "text/html");
+ // Skip empty notes
+ // TODO: Take into account image-only notes
+ if (!doc.body.textContent.trim()) {
+ continue;
+ }
+ if (!first) {
+ Zotero.write("\n\n---\n\n");
+ }
+ first = false;
+ Zotero.write(await bundle.convert(_Zotero, doc));
+ }
+ }
+}
diff --git a/src/events.ts b/src/events.ts
index f9ddb73..ab2de65 100644
--- a/src/events.ts
+++ b/src/events.ts
@@ -45,6 +45,9 @@ class AddonEvents extends AddonBase {
public async onInit() {
Zotero.debug("Knowledge4Zotero: init called");
await Zotero.uiReadyPromise;
+ // Init translator
+ // await loadTranslator(TRANSLATOR_ID_BETTER_MARKDOWN);
+ // Init UI
this._Addon.views.addOpenWorkspaceButton();
this._Addon.views.addNewKnowledgeButton();
this.addEditorInstanceListener();
@@ -1039,7 +1042,6 @@ class AddonEvents extends AddonBase {
await this._Addon.knowledge.exportNoteToFile(
message.content.editorInstance._item,
options.embedLink,
- options.embedImage,
options.exportFile,
options.exportNote,
options.exportCopy
diff --git a/src/export.ts b/src/export.ts
index 9994d6b..6cc3ea6 100644
--- a/src/export.ts
+++ b/src/export.ts
@@ -31,14 +31,6 @@ class AddonExport extends AddonBase {
) as XUL.Checkbox
).checked = embedLink;
}
- let embedImage = Zotero.Prefs.get("Knowledge4Zotero.embedImage");
- if (typeof embedImage !== "undefined") {
- (
- this._window.document.getElementById(
- "Knowledge4Zotero-export-embedImage"
- ) as XUL.Checkbox
- ).checked = embedImage;
- }
let exportNote = Zotero.Prefs.get("Knowledge4Zotero.exportNote");
if (typeof exportNote !== "undefined") {
(
@@ -71,11 +63,6 @@ class AddonExport extends AddonBase {
"Knowledge4Zotero-export-embedLink"
) as XUL.Checkbox
).checked;
- let embedImage = (
- this._window.document.getElementById(
- "Knowledge4Zotero-export-embedImage"
- ) as XUL.Checkbox
- ).checked;
let exportNote = (
this._window.document.getElementById(
"Knowledge4Zotero-export-enablenote"
@@ -88,7 +75,6 @@ class AddonExport extends AddonBase {
).checked;
Zotero.Prefs.set("Knowledge4Zotero.exportFile", exportFile);
Zotero.Prefs.set("Knowledge4Zotero.embedLink", embedLink);
- Zotero.Prefs.set("Knowledge4Zotero.embedImage", embedImage);
Zotero.Prefs.set("Knowledge4Zotero.exportNote", exportNote);
Zotero.Prefs.set("Knowledge4Zotero.exportCopy", exportCopy);
Zotero.debug(this.io);
@@ -96,7 +82,6 @@ class AddonExport extends AddonBase {
this.io.dataOut = {
exportFile: exportFile,
embedLink: embedLink,
- embedImage: embedImage,
exportNote: exportNote,
exportCopy: exportCopy,
};
diff --git a/src/exportMD.ts b/src/exportMD.ts
new file mode 100644
index 0000000..20447b6
--- /dev/null
+++ b/src/exportMD.ts
@@ -0,0 +1,34 @@
+const TRANSLATOR_ID_BETTER_MARKDOWN = "1412e9e2-51e1-42ec-aa35-e036a895534c";
+
+const configs = {};
+
+configs[TRANSLATOR_ID_BETTER_MARKDOWN] = {
+ translatorID: TRANSLATOR_ID_BETTER_MARKDOWN,
+ label: "Better Note Markdown",
+ creator: "Martynas Bagdonas; Winding",
+ target: "md",
+ minVersion: "5.0.97",
+ maxVersion: "",
+ priority: 50,
+ configOptions: {
+ noteTranslator: true,
+ },
+ displayOptions: {
+ includeAppLinks: true,
+ },
+ inRepository: true,
+ translatorType: 2,
+ lastUpdated: "2022-06-01 10:26:46",
+ _codePath:
+ "chrome://Knowledge4Zotero/content/translators/Better Note Markdown.js",
+};
+
+async function loadTranslator(id) {
+ const config = configs[id];
+ const code = (await Zotero.File.getContentsAsync(config._codePath)).response;
+ Zotero.debug(code);
+ await Zotero.Translators.save(config, code);
+ await Zotero.Translators.reinit();
+}
+
+export { TRANSLATOR_ID_BETTER_MARKDOWN, loadTranslator };
diff --git a/src/file_picker.ts b/src/file_picker.ts
new file mode 100644
index 0000000..c6fec0e
--- /dev/null
+++ b/src/file_picker.ts
@@ -0,0 +1,33 @@
+export async function pick(title: string, mode: 'open' | 'save' | 'folder', filters?: [string, string][], suggestion?: string): Promise {
+ const fp = Components.classes['@mozilla.org/filepicker;1'].createInstance(Components.interfaces.nsIFilePicker)
+
+ if (suggestion) fp.defaultString = suggestion
+
+ mode = {
+ open: Components.interfaces.nsIFilePicker.modeOpen,
+ save: Components.interfaces.nsIFilePicker.modeSave,
+ folder: Components.interfaces.nsIFilePicker.modeGetFolder,
+ }[mode]
+
+ fp.init(window, title, mode)
+
+ for (const [label, ext] of (filters || [])) {
+ fp.appendFilter(label, ext)
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
+ return new Zotero.Promise(resolve => {
+ fp.open(userChoice => {
+ switch (userChoice) {
+ case Components.interfaces.nsIFilePicker.returnOK:
+ case Components.interfaces.nsIFilePicker.returnReplace:
+ resolve(fp.file.path)
+ break
+
+ default: // aka returnCancel
+ resolve('')
+ break
+ }
+ })
+ })
+ }
\ No newline at end of file
diff --git a/src/knowledge.ts b/src/knowledge.ts
index ab6cbf0..fe71baa 100644
--- a/src/knowledge.ts
+++ b/src/knowledge.ts
@@ -1,4 +1,6 @@
import { AddonBase, EditorMessage, OutlineType } from "./base";
+import { loadTranslator, TRANSLATOR_ID_BETTER_MARKDOWN } from "./exportMD";
+import { pick } from "./file_picker";
const TreeModel = require("./treemodel");
@@ -6,6 +8,8 @@ class Knowledge extends AddonBase {
currentLine: number;
currentNodeID: number;
workspaceWindow: Window;
+ _exportNote: ZoteroItem;
+ _exportPath: string;
constructor(parent: Knowledge4Zotero) {
super(parent);
this.currentLine = -1;
@@ -623,7 +627,6 @@ class Knowledge extends AddonBase {
async exportNoteToFile(
note: ZoteroItem,
convertNoteLinks: boolean = true,
- convertNoteImages: boolean = true,
saveFile: boolean = true,
saveNote: boolean = false,
saveCopy: boolean = false
@@ -632,29 +635,67 @@ class Knowledge extends AddonBase {
return;
}
note = note || this.getWorkspaceNote();
- const noteID = await ZoteroPane_Local.newNote();
- const newNote = Zotero.Items.get(noteID);
- const rootNoteIds = [note.id];
+ let newNote: ZoteroItem;
+ if (convertNoteLinks || saveNote) {
+ const noteID = await ZoteroPane_Local.newNote();
+ newNote = Zotero.Items.get(noteID);
+ const rootNoteIds = [note.id];
- const convertResult = await this.convertNoteLines(
- note,
- rootNoteIds,
- convertNoteLinks,
- convertNoteImages
- );
+ const convertResult = await this.convertNoteLines(
+ note,
+ rootNoteIds,
+ convertNoteLinks
+ );
- this.setLinesToNote(newNote, convertResult.lines);
- Zotero.debug(convertResult.subNotes);
+ this.setLinesToNote(newNote, convertResult.lines);
+ Zotero.debug(convertResult.subNotes);
+
+ await Zotero.DB.executeTransaction(async () => {
+ for (const subNote of convertResult.subNotes) {
+ await Zotero.Notes.copyEmbeddedImages(subNote, newNote);
+ }
+ });
+ } else {
+ newNote = note;
+ }
- await Zotero.DB.executeTransaction(async () => {
- for (const subNote of convertResult.subNotes) {
- await Zotero.Notes.copyEmbeddedImages(subNote, newNote);
- }
- });
if (saveFile) {
- const exporter = new Zotero_File_Exporter();
- exporter.items = [newNote];
- await exporter.save();
+ if (
+ (await new Zotero.Translate.Export().getTranslators()).filter(
+ (e) => e.translatorID === TRANSLATOR_ID_BETTER_MARKDOWN
+ )
+ ) {
+ await loadTranslator(TRANSLATOR_ID_BETTER_MARKDOWN);
+ }
+
+ const filename = await pick(
+ Zotero.getString("fileInterface.export"),
+ "save",
+ [["MarkDown File(*.md)", "*.md"]],
+ `${newNote.getNoteTitle()}.md`
+ );
+ if (!filename) {
+ return;
+ }
+
+ this._exportNote = newNote;
+ this._exportPath =
+ Zotero.File.pathToFile(filename).parent.path + "\\attachments";
+
+ const hasImage = newNote.getNote().includes("
");
- const imageKeyReg = new RegExp(`data-attachment-key="`);
- const imageAnnotationReg = new RegExp(`data-annotation="`);
-
- const imageIndex = line.search(imageReg);
- if (imageIndex !== -1) {
- const lineStart = line.slice(0, imageIndex);
- const imageLine = line.slice(imageIndex);
- const lineEnd = imageLine.slice(imageLine.search(imageBrReg));
- const attachmentKeyIndex = imageLine.search(imageKeyReg);
- const annotationIndex = imageLine.search(imageAnnotationReg);
-
- if (attachmentKeyIndex !== -1) {
- let attachmentKey = imageLine.slice(
- attachmentKeyIndex + imageKeyReg.source.length
- );
- attachmentKey = attachmentKey.slice(0, attachmentKey.search(/"/g));
- const attachmentItem = await Zotero.Items.getByLibraryAndKeyAsync(
- sourceNote.libraryID,
- attachmentKey
- );
- let attachmentURL = await attachmentItem.getFilePathAsync();
- if (attachmentURL) {
- Zotero.debug("convert image");
- // const imageData = await editorInstance._getDataURL(
- // attachmentItem
- // );
- Zotero.debug(line);
- Zotero.debug(lineStart);
- Zotero.debug(lineEnd);
- if (Zotero.isMac) {
- attachmentURL = "file://" + attachmentURL;
- }
- newLines.push(`!image
`);
-
- // Export annotation link
- if (annotationIndex !== -1) {
- let annotationContentRaw = imageLine.slice(
- annotationIndex + imageAnnotationReg.source.length
- );
- annotationContentRaw = annotationContentRaw.slice(
- 0,
- annotationContentRaw.search('"')
- );
- if (annotationContentRaw) {
- Zotero.debug("convert image annotation");
- Zotero.debug(annotationContentRaw);
- try {
- let annotation = JSON.parse(
- decodeURIComponent(annotationContentRaw)
- );
- if (annotation) {
- // 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 annotationURL;
- let uriParts = uri.split("/");
- let libraryType = uriParts[3];
- let key = uriParts[6];
- if (libraryType === "users") {
- annotationURL = "zotero://open-pdf/library/items/" + key;
- }
- // groups
- else {
- let groupID = uriParts[4];
- annotationURL =
- "zotero://open-pdf/groups/" + groupID + "/items/" + key;
- }
-
- annotationURL +=
- "?page=" +
- (position.pageIndex + 1) +
- (annotation.annotationKey
- ? "&annotation=" + annotation.annotationKey
- : "");
- newLines.push(`pdf
`);
- }
- }
- } catch (e) {
- Zotero.debug(e);
- }
- }
- }
- newLines.push(`${lineStart}${lineEnd}`);
- return true;
- }
- }
- }
- return false;
- }
-
async convertNoteLines(
currentNote: ZoteroItem,
rootNoteIds: number[],
- convertNoteLinks: boolean = true,
- convertNoteImages: boolean = true
+ convertNoteLinks: boolean = true
): Promise<{ lines: string[]; subNotes: ZoteroItem[] }> {
Zotero.debug(`convert note ${currentNote.id}`);
@@ -792,17 +740,6 @@ class Knowledge extends AddonBase {
let newLines = [];
const noteLines = this.getLinesInNote(currentNote);
for (let i in noteLines) {
- // Embed Image
- if (convertNoteImages) {
- const hasImage = await this.convertImage(
- noteLines[i],
- newLines,
- currentNote
- );
- if (hasImage) {
- continue;
- }
- }
newLines.push(noteLines[i]);
// Convert Link
if (convertNoteLinks) {
@@ -816,8 +753,7 @@ class Knowledge extends AddonBase {
const convertResult = await this.convertNoteLines(
subNote,
_rootNoteIds,
- convertNoteLinks,
- convertNoteImages
+ convertNoteLinks
);
const subNoteLines = convertResult.lines;
let _newLine: string = "";