From fa193ad963b9ae95fb42786cbbdaaad14f3314f3 Mon Sep 17 00:00:00 2001 From: xiangyu <3170102889@zju.edu.cn> Date: Thu, 22 Sep 2022 16:05:25 +0800 Subject: [PATCH] change: export md fix: reader note editor not initialized add: export md with citation wrapped fix: MDFileName template bug --- .../translators/Better Note Markdown.js | 1914 ----------------- package.json | 6 +- src/events.ts | 15 +- src/exportMD.ts | 34 - src/knowledge.ts | 74 +- src/parse.ts | 285 +++ src/template.ts | 2 +- tsconfig.json | 3 +- 8 files changed, 326 insertions(+), 2007 deletions(-) delete mode 100644 addon/chrome/content/translators/Better Note Markdown.js delete mode 100644 src/exportMD.ts diff --git a/addon/chrome/content/translators/Better Note Markdown.js b/addon/chrome/content/translators/Better Note Markdown.js deleted file mode 100644 index 479a79c..0000000 --- a/addon/chrome/content/translators/Better Note Markdown.js +++ /dev/null @@ -1,1914 +0,0 @@ -/* - ***** 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"; - var _Zotero = Components.classes[ - "@zotero.org/Zotero;1" - ].getService(Components.interfaces.nsISupports).wrappedJSObject; - - 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.mathBlock = { - filter: function (node) { - return node.nodeName === "PRE" && node.className === "math"; - }, - - replacement: function (content, node, options) { - return ( - "\n\n$$\n" + - node.firstChild.textContent.slice(2, -2) + - "\n$$\n\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 + '"'; - if (href.search(/zotero:\/\/note\/\w+\/\w+\//g) !== -1) { - // A note link should be converted if it is in the _exportFileDict - var _Zotero = Components.classes[ - "@zotero.org/Zotero;1" - ].getService( - Components.interfaces.nsISupports - ).wrappedJSObject; - const noteInfo = - _Zotero.Knowledge4Zotero.knowledge._exportFileDict && - _Zotero.Knowledge4Zotero.knowledge._exportFileDict.find( - (i) => href.includes(i.link) - ); - if (noteInfo) { - href = `./${noteInfo.filename}`; - } - } - 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 + ")" - : ""; - }, - }; - - rules.backgroundColor = { - filter: function (node) { - return ( - node.nodeName === "SPAN" && - node.style["background-color"] && - _Zotero.Prefs.get("Knowledge4Zotero.exportHighlight") - ); - }, - - replacement: function (content, node) { - return `${content}`; - }, - }; - - 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, "\\_"], - [/^(\d+)\. /g, "$1\\. "], - ]; - - if (_Zotero.Prefs.get("Knowledge4Zotero.convertSquare")) { - escapes.push([/\[/g, "\\["]); - escapes.push([/\]/g, "\\]"]); - } - - 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, item, doc) { - Components.utils.import("resource://gre/modules/osfile.jsm"); - // 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) { - Zotero.debug(node.outerHTML); - try { - var annotation = JSON.parse( - decodeURIComponent(node.getAttribute("data-annotation")) - ); - } catch (e) { - Zotero.debug(e); - } - - if (annotation) { - // 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); - if ( - Zotero.getOption("includeAppLinks") && - 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 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"); - try { - var annotation = JSON.parse( - decodeURIComponent(img.getAttribute("data-annotation")) - ); - } catch (e) { - Zotero.debug(e); - } - - if (!annotation) { - continue; - } - - const params = {}; - const router = new _Zotero.Router(params); - router.add("zotero.org/users/:libKey/items/:itemKey", () => { - params.libKey = 1; - }); - router.add("zotero.org/users/local/:libKey/items/:itemKey", () => { - params.libKey = 1; - }); - router.add("zotero.org/groups/:libKey/items/:itemKey", () => { - params.libKey = _Zotero.Groups.getLibraryIDFromGroupID( - params.libKey - ); - }); - router.run(annotation.attachmentURI.split("://").pop()); - console.log(params); - console.log(item); - - const attachmentItem = await _Zotero.Items.getByLibraryAndKeyAsync( - params.libKey, - imgKey - ); - Zotero.debug(attachmentItem); - - let oldFile = String(await attachmentItem.getFilePathAsync()); - Zotero.debug(oldFile); - let ext = oldFile.split(".").pop(); - let newAbsPath = OS.Path.join( - ...`${_Zotero.Knowledge4Zotero.knowledge._exportPath}/${imgKey}.${ext}`.split( - /\// - ) - ); - if (!_Zotero.isWin && newAbsPath.charAt(0) !== "/") { - newAbsPath = "/" + newAbsPath; - } - Zotero.debug(newAbsPath); - let newFile = oldFile; - try { - // Don't overwrite - if (await OS.File.exists(newAbsPath)) { - newFile = newAbsPath.replace(/\\/g, "/"); - } else { - newFile = _Zotero.File.copyToUnique(oldFile, newAbsPath).path; - newFile = newFile.replace(/\\/g, "/"); - } - newFile = `attachments/${newFile.split(/\//).pop()}`; - } catch (e) { - Zotero.debug(e); - } - Zotero.debug(newFile); - - img.setAttribute("src", newFile ? newFile : oldFile); - 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[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 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() { - var _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, item, doc)); - } - } - // Resolve promise - _Zotero.Knowledge4Zotero.knowledge._exportPromise.resolve(); -} diff --git a/package.json b/package.json index 3af6811..c46e321 100644 --- a/package.json +++ b/package.json @@ -32,11 +32,15 @@ "compressing": "^1.5.1", "esbuild": "^0.14.34", "replace-in-file": "^6.3.2", - "tree-model": "^1.0.7" + "seedrandom": "^3.0.5", + "tree-model": "^1.0.7", + "turndown": "^7.1.1", + "turndown-plugin-gfm": "^1.0.2" }, "devDependencies": { "@types/jquery": "^3.5.14", "@types/node": "^17.0.31", + "@types/turndown": "^5.0.1", "release-it": "^14.14.0", "zotero-types": "^0.0.4" } diff --git a/src/events.ts b/src/events.ts index 3d0a788..fdb5ebe 100644 --- a/src/events.ts +++ b/src/events.ts @@ -76,7 +76,7 @@ class AddonEvents extends AddonBase { t < 100 ) { t += 1; - this._Addon.knowledge.setWorkspaceNote(); + this._Addon.knowledge.setWorkspaceNote("main", undefined, false); await Zotero.Promise.delay(100); } @@ -500,15 +500,15 @@ class AddonEvents extends AddonBase { editor._knowledgeUIInitialized = false; const currentID = editor._item.id; + const noteItem = editor._item; + // item.getNote may not be initialized yet + if (Zotero.ItemTypes.getID("note") !== noteItem.itemTypeID) { + return; + } // Check if this is a window for print const isPrint = this._Addon.knowledge._pdfNoteId === currentID; - const noteItem = Zotero.Items.get(currentID) as Zotero.Item; - if (!noteItem.isNote()) { - return; - } - const mainNoteID = parseInt( Zotero.Prefs.get("Knowledge4Zotero.mainKnowledgeID") as string ); @@ -1843,6 +1843,9 @@ class AddonEvents extends AddonBase { await io.deferred.promise; const options = io.dataOut as any; + if (!options) { + return; + } if (options.exportFile && options.exportSingleFile) { await this._Addon.knowledge.exportNotesToFile( [item], diff --git a/src/exportMD.ts b/src/exportMD.ts deleted file mode 100644 index e1ba942..0000000 --- a/src/exportMD.ts +++ /dev/null @@ -1,34 +0,0 @@ -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 23: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/knowledge.ts b/src/knowledge.ts index c9e3ec1..da03533 100644 --- a/src/knowledge.ts +++ b/src/knowledge.ts @@ -1,6 +1,5 @@ import Knowledge4Zotero from "./addon"; import { OutlineType } from "./base"; -import { loadTranslator, TRANSLATOR_ID_BETTER_MARKDOWN } from "./exportMD"; import { pick } from "./file_picker"; import AddonBase from "./module"; @@ -15,7 +14,6 @@ class Knowledge extends AddonBase { _workspacePromise: any; _exportPath: string; _exportFileDict: object; - _exportPromise: any; _pdfNoteId: number; _pdfPrintPromise: any; constructor(parent: Knowledge4Zotero) { @@ -191,7 +189,8 @@ class Knowledge extends AddonBase { async setWorkspaceNote( type: "main" | "preview" = "main", - note: Zotero.Item | undefined = undefined + note: Zotero.Item | undefined = undefined, + showPopup: boolean = true ) { let _window = this.getWorkspaceWindow() as Window; note = note || this.getWorkspaceNote(); @@ -273,10 +272,12 @@ class Knowledge extends AddonBase { .filter((id) => id) .join(",") ); - this._Addon.views.showProgressWindow( - "Better Notes", - `Set main Note to: ${note.getNoteTitle()}` - ); + if (showPopup) { + this._Addon.views.showProgressWindow( + "Better Notes", + `Set main Note to: ${note.getNoteTitle()}` + ); + } } } @@ -786,12 +787,12 @@ class Knowledge extends AddonBase { async exportNoteToFile( note: Zotero.Item, convertNoteLinks: boolean = true, - saveFile: boolean = true, + saveMD: boolean = true, saveNote: boolean = false, - saveCopy: boolean = false, + doCopy: boolean = false, savePDF: boolean = false ) { - if (!saveFile && !saveNote && !saveCopy && !savePDF) { + if (!saveMD && !saveNote && !doCopy && !savePDF) { return; } this._exportFileDict = []; @@ -822,9 +823,7 @@ class Knowledge extends AddonBase { newNote = note; } - if (saveFile) { - await loadTranslator(TRANSLATOR_ID_BETTER_MARKDOWN); - + if (saveMD) { const filename = await pick( Zotero.getString("fileInterface.export"), "save", @@ -836,30 +835,10 @@ class Knowledge extends AddonBase { Zotero.File.pathToFile(filename).parent.path + "/attachments"; // Convert to unix format this._exportPath = this._exportPath.replace(/\\/g, "/"); - - Components.utils.import("resource://gre/modules/osfile.jsm"); - - const hasImage = newNote.getNote().includes(" { const noteLines = this._Addon.knowledge.getLinesInNote(note); let tree = new TreeModel(); @@ -431,6 +450,272 @@ class AddonParse extends AddonBase { parseAsciiDocToHTML(str: string): string { return asciidoctor.convert(str); } + + // A realization of Markdown Note.js translator + async parseNoteToMD(noteItem: Zotero.Item, options: any = {}) { + const doc = new DOMParser().parseFromString( + noteItem.getNote() || "", + "text/html" + ); + Components.utils.import("resource://gre/modules/osfile.jsm"); + 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((node) => { + Zotero.debug(node.outerHTML); + try { + var annotation = JSON.parse( + decodeURIComponent(node.getAttribute("data-annotation")) + ); + } catch (e) { + Zotero.debug(e); + } + + if (annotation) { + // 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); + 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 a = doc.createElement("a"); + a.href = openURI; + a.append("pdf"); + let fragment = doc.createDocumentFragment(); + fragment.append(" (", a, ") "); + + if (options.wrapCitation) { + const citationKey = annotation.annotationKey + ? annotation.annotationKey + : this.randomString( + 8, + Zotero.Utilities.allowedKeyChars, + Zotero.Utilities.Internal.md5( + node.getAttribute("data-annotation") + ) + ); + Zotero.Utilities.Internal.md5( + node.getAttribute("data-annotation") + ); + const beforeCitationDecorator = doc.createElement("span"); + beforeCitationDecorator.innerHTML = `<!-- bn::${citationKey} -->`; + const afterCitationDecorator = doc.createElement("span"); + afterCitationDecorator.innerHTML = `<!-- bn::${citationKey} -->`; + node.before(beforeCitationDecorator); + fragment.append(afterCitationDecorator); + } + + let nextNode = node.nextElementSibling; + if (nextNode && nextNode.classList.contains("citation")) { + nextNode.parentNode.insertBefore(fragment, nextNode.nextSibling); + } else { + node.parentNode.insertBefore(fragment, node.nextSibling); + } + } + } + }); + + console.log(doc); + + for (const img of doc.querySelectorAll("img[data-attachment-key]")) { + let imgKey = img.getAttribute("data-attachment-key"); + + const attachmentItem = await Zotero.Items.getByLibraryAndKeyAsync( + noteItem.libraryID, + imgKey + ); + Zotero.debug(attachmentItem); + + let oldFile = String(await attachmentItem.getFilePathAsync()); + Zotero.debug(oldFile); + let ext = oldFile.split(".").pop(); + let newAbsPath = OS.Path.join( + ...`${this._Addon.knowledge._exportPath}/${imgKey}.${ext}`.split(/\//) + ); + if (!Zotero.isWin && newAbsPath.charAt(0) !== "/") { + newAbsPath = "/" + newAbsPath; + } + Zotero.debug(newAbsPath); + let newFile = oldFile; + try { + // Don't overwrite + if (await OS.File.exists(newAbsPath)) { + newFile = newAbsPath.replace(/\\/g, "/"); + } else { + newFile = Zotero.File.copyToUnique(oldFile, newAbsPath).path; + newFile = newFile.replace(/\\/g, "/"); + } + newFile = `attachments/${newFile.split(/\//).pop()}`; + } catch (e) { + Zotero.debug(e); + } + Zotero.debug(newFile); + + img.setAttribute("src", newFile ? newFile : oldFile); + 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[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 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 `${item}`; + }) + .join("; ") + + ")"; + } + }); + // Overwrite escapes + const escapes: [RegExp, string][] = [ + // [/\\/g, '\\\\'], + // [/\*/g, '\\*'], + [/^-/g, "\\-"], + [/^\+ /g, "\\+ "], + [/^(=+)/g, "\\$1"], + [/^(#{1,6}) /g, "\\$1 "], + [/`/g, "\\`"], + [/^~~~/g, "\\~~~"], + // [/^>/g, "\\>"], + // [/_/g, "\\_"], + [/^(\d+)\. /g, "$1\\. "], + ]; + if (Zotero.Prefs.get("Knowledge4Zotero.convertSquare")) { + escapes.push([/\[/g, "\\["]); + escapes.push([/\]/g, "\\]"]); + } + TurndownService.prototype.escape = function (string) { + return escapes.reduce(function (accumulator, escape) { + return accumulator.replace(escape[0], escape[1]); + }, string); + }; + // Initialize Turndown Service + let turndownService = new TurndownService({ + headingStyle: "atx", + bulletListMarker: "-", + emDelimiter: "*", + codeBlockStyle: "fenced", + }); + turndownService.use(turndownPluginGfm.gfm); + // Add math block rule + turndownService.addRule("mathBlock", { + filter: function (node) { + return node.nodeName === "PRE" && node.className === "math"; + }, + + replacement: function (content, node, options) { + return ( + "\n\n$$\n" + node.firstChild.textContent.slice(2, -2) + "\n$$\n\n" + ); + }, + }); + turndownService.addRule("inlineLinkCustom", { + filter: function (node, options) { + return ( + options.linkStyle === "inlined" && + node.nodeName === "A" && + node.getAttribute("href").length > 0 + ); + }, + + replacement: function (content, node: HTMLElement, options) { + var href = node.getAttribute("href"); + const cleanAttribute = (attribute) => + attribute ? attribute.replace(/(\n+\s*)+/g, "\n") : ""; + var title = cleanAttribute(node.getAttribute("title")); + if (title) title = ' "' + title + '"'; + if (href.search(/zotero:\/\/note\/\w+\/\w+\//g) !== -1) { + // A note link should be converted if it is in the _exportFileDict + var _Zotero = Components.classes["@zotero.org/Zotero;1"].getService( + Components.interfaces.nsISupports + ).wrappedJSObject; + const noteInfo = + _Zotero.Knowledge4Zotero.knowledge._exportFileDict && + _Zotero.Knowledge4Zotero.knowledge._exportFileDict.find((i) => + href.includes(i.link) + ); + if (noteInfo) { + href = `./${noteInfo.filename}`; + } + } + return "[" + content + "](" + href + title + ")"; + }, + }); + return turndownService.turndown(doc.body); + } } export default AddonParse; diff --git a/src/template.ts b/src/template.ts index 9c7cb12..0c8e11a 100644 --- a/src/template.ts +++ b/src/template.ts @@ -38,7 +38,7 @@ class AddonTemplate extends AddonBase { }, { name: "[ExportMDFileName]", - text: '${(noteItem.getNoteTitle ? noteItem.getNoteTitle().replace(/[/\\?%*:|"<> ]/g, "-") + "-" : "")}${noteItem.key}.md', + text: '${(noteItem.getNoteTitle ? noteItem.getNoteTitle().replace(/[/\\\\?%*:|"<> ]/g, "-") + "-" : "")}${noteItem.key}.md', disabled: false, }, { diff --git a/tsconfig.json b/tsconfig.json index 3b7eef9..8441781 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,8 @@ "include": [ "src", "typing", - "node_modules/zotero-types" + "node_modules/zotero-types", + ], "exclude": [ "builds",