From 19977598eb1654fed5511d79406d5e21cde811f9 Mon Sep 17 00:00:00 2001 From: Martynas Bagdonas Date: Mon, 22 Nov 2021 15:41:35 +0200 Subject: [PATCH 1/3] Markdown note export (#2214) --- chrome/content/zotero/exportOptions.js | 28 ++- chrome/content/zotero/fileInterface.js | 182 ++++++++++++++---- chrome/content/zotero/itemTree.jsx | 55 ++++-- .../zotero/preferences/preferences_export.jsx | 77 ++++++++ .../zotero/preferences/preferences_export.xul | 10 +- .../content/zotero/standalone/standalone.js | 25 +-- .../content/zotero/standalone/standalone.xul | 5 + chrome/content/zotero/xpcom/quickCopy.js | 182 +++--------------- chrome/content/zotero/xpcom/translate | 2 +- .../xpcom/translation/translate_item.js | 5 +- chrome/content/zotero/zoteroPane.js | 36 +++- chrome/locale/en-US/zotero/preferences.dtd | 3 +- chrome/locale/en-US/zotero/standalone.dtd | 1 + chrome/locale/en-US/zotero/zotero.properties | 2 + defaults/preferences/zotero.js | 8 +- test/tests/quickCopyTest.js | 11 -- translators | 2 +- 17 files changed, 383 insertions(+), 251 deletions(-) diff --git a/chrome/content/zotero/exportOptions.js b/chrome/content/zotero/exportOptions.js index 9919f9915f..2474815713 100644 --- a/chrome/content/zotero/exportOptions.js +++ b/chrome/content/zotero/exportOptions.js @@ -49,8 +49,7 @@ var Zotero_File_Interface_Export = new function() { var addedOptions = new Object(); - var translators = window.arguments[0].translators; - translators.sort(function(a, b) { return a.label.localeCompare(b.label) }); + var { translators, exportingNotes } = window.arguments[0]; // get format popup var formatPopup = document.getElementById("format-popup"); @@ -58,7 +57,9 @@ var Zotero_File_Interface_Export = new function() { var optionsBox = document.getElementById("translator-options"); var charsetBox = document.getElementById("charset-box"); - var selectedTranslator = Zotero.Prefs.get("export.lastTranslator"); + var selectedTranslator = Zotero.Prefs.get( + exportingNotes ? "export.lastNoteTranslator" : "export.lastTranslator" + ); // add styles to format popup for(var i in translators) { @@ -72,7 +73,12 @@ var Zotero_File_Interface_Export = new function() { // presented to the user // get readable name for option try { - var optionLabel = Zotero.getString("exportOptions."+option); + if (option == 'includeAppLinks') { + var optionLabel = Zotero.getString("exportOptions." + option, Zotero.appName); + } + else { + var optionLabel = Zotero.getString("exportOptions." + option); + } } catch(e) { var optionLabel = option; } @@ -121,7 +127,9 @@ var Zotero_File_Interface_Export = new function() { _charsets = Zotero_Charset_Menu.populate(document.getElementById(OPTION_PREFIX+"exportCharset"), true); } - this.updateOptions(Zotero.Prefs.get("export.translatorSettings")); + this.updateOptions(Zotero.Prefs.get( + exportingNotes ? "export.noteTranslatorSettings" : "export.translatorSettings" + )); } /* @@ -224,7 +232,10 @@ var Zotero_File_Interface_Export = new function() { window.arguments[0].selectedTranslator = window.arguments[0].translators[index]; // save selected translator - Zotero.Prefs.set("export.lastTranslator", window.arguments[0].translators[index].translatorID); + Zotero.Prefs.set( + window.arguments[0].exportingNotes ? "export.lastNoteTranslator" : "export.lastTranslator", + window.arguments[0].translators[index].translatorID + ); // set options on selected translator and generate optionString var optionsAvailable = window.arguments[0].selectedTranslator.displayOptions; @@ -253,7 +264,10 @@ var Zotero_File_Interface_Export = new function() { // save options var optionString = JSON.stringify(displayOptions); - Zotero.Prefs.set("export.translatorSettings", optionString); + Zotero.Prefs.set( + window.arguments[0].exportingNotes ? "export.noteTranslatorSettings" : "export.translatorSettings", + optionString + ); } /* diff --git a/chrome/content/zotero/fileInterface.js b/chrome/content/zotero/fileInterface.js index 9e0e228048..2b2b484c95 100644 --- a/chrome/content/zotero/fileInterface.js +++ b/chrome/content/zotero/fileInterface.js @@ -49,9 +49,42 @@ var Zotero_File_Exporter = function() { Zotero_File_Exporter.prototype.save = async function () { var translation = new Zotero.Translate.Export(); var translators = await translation.getTranslators(); + + if (!this.items) { + return; + } + + let exportingNotes = this.items.every(item => item.isNote() || item.isAttachment()); + // Keep only note export and Zotero RDF translators, if all items are notes or attachments + if (exportingNotes) { + translators = translators.filter((translator) => { + return ( + translator.translatorID === '14763d24-8ba0-45df-8f52-b8d1108e7ac9' + || translator.configOptions && translator.configOptions.noteTranslator + ); + }); + } + // Otherwise exclude note export translators + else { + translators = translators.filter(t => !t.configOptions || !t.configOptions.noteTranslator); + } + + translators.sort((a, b) => a.label.localeCompare(b.label)); + + // Remove "Note" prefix from Note Markdown and Note HTML translators + let markdownTranslator = translators.find(t => t.translatorID == '154c2785-ec83-4c27-8a8a-d27b3a2eded1'); + if (markdownTranslator) { + markdownTranslator.label = 'Markdown'; + // Move Note Markdown translator to the top + translators.unshift(...translators.splice(translators.indexOf(markdownTranslator), 1)); + } + let htmlTranslator = translators.find(t => t.translatorID == '897a81c2-9f60-4bec-ae6b-85a5030b8be5'); + if (htmlTranslator) { + htmlTranslator.label = 'HTML'; + } // present options dialog - var io = {translators:translators} + var io = { translators, exportingNotes }; window.openDialog("chrome://zotero/content/exportOptions.xul", "_blank", "chrome,modal,centerscreen,resizable=no", io); if(!io.selectedTranslator) { @@ -89,33 +122,49 @@ Zotero_File_Exporter.prototype.save = async function () { translation.setLibraryID(this.libraryID); } - translation.setLocation(Zotero.File.pathToFile(fp.file)); + async function _exportDone(obj, worked) { + if (!worked) { + Zotero.alert( + null, + Zotero.getString('general.error'), + Zotero.getString('fileInterface.exportError') + ); + Zotero_File_Interface.Progress.close(); + return; + } + + // For Note Markdown translator replace zotero:// URI scheme, + // if the current app is not Zotero + if (io.selectedTranslator.translatorID == '154c2785-ec83-4c27-8a8a-d27b3a2eded1' + && ZOTERO_CONFIG.ID != 'zotero') { + let text = obj.string; + text = text.replace(/zotero:\/\//g, ZOTERO_CONFIG.ID + '://'); + await Zotero.File.putContentsAsync(fp.file, text); + } + + // Close the items exported indicator + Zotero_File_Interface.Progress.close(); + } + + // Post process and save translator output in _exportDone, if using + // Note Markdown translator and the current app is not Zotero. + // For other translators setLocation is better because it uses streaming + if (!(io.selectedTranslator.translatorID == '154c2785-ec83-4c27-8a8a-d27b3a2eded1' + && ZOTERO_CONFIG.ID != 'zotero')) { + translation.setLocation(Zotero.File.pathToFile(fp.file)); + } translation.setTranslator(io.selectedTranslator); translation.setDisplayOptions(io.displayOptions); translation.setHandler("itemDone", function () { Zotero.updateZoteroPaneProgressMeter(translation.getProgress()); }); - translation.setHandler("done", this._exportDone); + translation.setHandler("done", _exportDone); Zotero_File_Interface.Progress.show( Zotero.getString("fileInterface.itemsExported") ); translation.translate() }; - -/* - * Closes the items exported indicator - */ -Zotero_File_Exporter.prototype._exportDone = function(obj, worked) { - Zotero_File_Interface.Progress.close(); - - if(!worked) { - Zotero.alert( - null, - Zotero.getString('general.error'), - Zotero.getString("fileInterface.exportError") - ); - } -} + /****Zotero_File_Interface**** ** @@ -175,36 +224,91 @@ var Zotero_File_Interface = new function() { */ function exportItems() { var exporter = new Zotero_File_Exporter(); - - exporter.items = ZoteroPane_Local.getSelectedItems(); + let itemIDs = ZoteroPane_Local.getSelectedItems(true); + // Get selected item IDs in the item tree order + itemIDs = ZoteroPane_Local.getSortedItems(true).filter(id => itemIDs.includes(id)); + exporter.items = Zotero.Items.get(itemIDs); if(!exporter.items || !exporter.items.length) throw("no items currently selected"); exporter.save(); } + /* * exports items to clipboard */ function exportItemsToClipboard(items, translatorID) { - var translation = new Zotero.Translate.Export(); - translation.setItems(items); - translation.setTranslator(translatorID); - translation.setHandler("done", _copyToClipboard); - translation.translate(); - } - - /* - * handler when done exporting items to clipboard - */ - function _copyToClipboard(obj, worked) { - if(!worked) { - Zotero.alert( - null, Zotero.getString('general.error'), Zotero.getString("fileInterface.exportError") - ); - } else { - Components.classes["@mozilla.org/widget/clipboardhelper;1"] - .getService(Components.interfaces.nsIClipboardHelper) - .copyString(obj.string.replace(/\r\n/g, "\n")); + function _translate(items, translatorID, callback) { + let translation = new Zotero.Translate.Export(); + translation.setItems(items.slice()); + translation.setTranslator(translatorID); + translation.setHandler("done", callback); + translation.translate(); + } + + // If translating with virtual "Markdown + Rich Text" translator, use Note Markdown and + // Note HTML instead + if (translatorID == 'a45eca67-1ee8-45e5-b4c6-23fb8a852873') { + translatorID = '154c2785-ec83-4c27-8a8a-d27b3a2eded1'; + _translate(items, translatorID, (obj, worked) => { + if (!worked) { + Zotero.log(Zotero.getString('fileInterface.exportError'), 'warning'); + return; + } + translatorID = '897a81c2-9f60-4bec-ae6b-85a5030b8be5'; + _translate(items, translatorID, (obj2, worked) => { + if (!worked) { + Zotero.log(Zotero.getString('fileInterface.exportError'), 'warning'); + return; + } + + let text = obj.string.replace(/\r\n/g, '\n'); + let html = obj2.string.replace(/\r\n/g, '\n'); + + // copy to clipboard + let transferable = Components.classes['@mozilla.org/widget/transferable;1'] + .createInstance(Components.interfaces.nsITransferable); + let clipboardService = Components.classes['@mozilla.org/widget/clipboard;1'] + .getService(Components.interfaces.nsIClipboard); + + // Add Text + let str = Components.classes['@mozilla.org/supports-string;1'] + .createInstance(Components.interfaces.nsISupportsString); + str.data = text; + transferable.addDataFlavor('text/unicode'); + transferable.setTransferData('text/unicode', str, text.length * 2); + + // Add HTML + str = Components.classes['@mozilla.org/supports-string;1'] + .createInstance(Components.interfaces.nsISupportsString); + str.data = html; + transferable.addDataFlavor('text/html'); + transferable.setTransferData('text/html', str, html.length * 2); + + clipboardService.setData( + transferable, null, Components.interfaces.nsIClipboard.kGlobalClipboard + ); + }); + }); + } + else { + _translate(items, translatorID, (obj, worked) => { + if (!worked) { + Zotero.log(Zotero.getString('fileInterface.exportError'), 'warning'); + return; + } + let text = obj.string; + // For Note HTML translator use body content only + if (translatorID == '897a81c2-9f60-4bec-ae6b-85a5030b8be5') { + let parser = Components.classes['@mozilla.org/xmlextras/domparser;1'] + .createInstance(Components.interfaces.nsIDOMParser); + let doc = parser.parseFromString(text, 'text/html'); + text = doc.body.innerHTML; + } + Components.classes['@mozilla.org/widget/clipboardhelper;1'] + .getService(Components.interfaces.nsIClipboardHelper) + .copyString(text.replace(/\r\n/g, '\n')); + }); } } diff --git a/chrome/content/zotero/itemTree.jsx b/chrome/content/zotero/itemTree.jsx index d9154e0e8c..ed5133951b 100644 --- a/chrome/content/zotero/itemTree.jsx +++ b/chrome/content/zotero/itemTree.jsx @@ -1891,6 +1891,8 @@ var ItemTree = class ItemTree extends LibraryTree { event.dataTransfer.setDragImage(this._dragImageContainer, 0, 0); var itemIDs = this.getSelectedItems(true); + // Get selected item IDs in the item tree order + itemIDs = this.getSortedItems(true).filter(id => itemIDs.includes(id)); event.dataTransfer.setData("zotero/item", itemIDs); var items = Zotero.Items.get(itemIDs); @@ -1946,22 +1948,53 @@ var ItemTree = class ItemTree extends LibraryTree { // Get Quick Copy format for current URL (set via /ping from connector) var format = Zotero.QuickCopy.getFormatFromURL(Zotero.QuickCopy.lastActiveURL); - Zotero.debug("Dragging with format " + format); - - var exportCallback = function(obj, worked) { - if (!worked) { - Zotero.log(Zotero.getString("fileInterface.exportError"), 'warning'); - return; - } - - var text = obj.string.replace(/\r\n/g, "\n"); - event.dataTransfer.setData("text/plain", text); + // If all items are notes, use one of the note export translators + if (items.every(item => item.isNote())) { + format = Zotero.QuickCopy.getNoteFormat(); } + Zotero.debug("Dragging with format " + format); format = Zotero.QuickCopy.unserializeSetting(format); try { if (format.mode == 'export') { - Zotero.QuickCopy.getContentFromItems(items, format, exportCallback); + // If exporting with virtual "Markdown + Rich Text" translator, call Note Markdown + // and Note HTML translators instead + if (format.id === 'a45eca67-1ee8-45e5-b4c6-23fb8a852873') { + let markdownFormat = { mode: 'export', id: '154c2785-ec83-4c27-8a8a-d27b3a2eded1' }; + let htmlFormat = { mode: 'export', id: '897a81c2-9f60-4bec-ae6b-85a5030b8be5' }; + Zotero.QuickCopy.getContentFromItems(items, markdownFormat, (obj, worked) => { + if (!worked) { + Zotero.log(Zotero.getString('fileInterface.exportError'), 'warning'); + return; + } + Zotero.QuickCopy.getContentFromItems(items, htmlFormat, (obj2, worked) => { + if (!worked) { + Zotero.log(Zotero.getString('fileInterface.exportError'), 'warning'); + return; + } + event.dataTransfer.setData('text/plain', obj.string.replace(/\r\n/g, '\n')); + event.dataTransfer.setData('text/html', obj2.string.replace(/\r\n/g, '\n')); + }); + }); + } + else { + Zotero.QuickCopy.getContentFromItems(items, format, (obj, worked) => { + if (!worked) { + Zotero.log(Zotero.getString('fileInterface.exportError'), 'warning'); + return; + } + var text = obj.string.replace(/\r\n/g, '\n'); + // For Note HTML translator use body content only + if (format.id == '897a81c2-9f60-4bec-ae6b-85a5030b8be5') { + // Use body content only + let parser = Cc['@mozilla.org/xmlextras/domparser;1'] + .createInstance(Ci.nsIDOMParser); + let doc = parser.parseFromString(text, 'text/html'); + text = doc.body.innerHTML; + } + event.dataTransfer.setData('text/plain', text); + }); + } } else if (format.mode == 'bibliography') { var content = Zotero.QuickCopy.getContentFromItems(items, format, null, event.shiftKey); diff --git a/chrome/content/zotero/preferences/preferences_export.jsx b/chrome/content/zotero/preferences/preferences_export.jsx index 6176f2f951..94ec430865 100644 --- a/chrome/content/zotero/preferences/preferences_export.jsx +++ b/chrome/content/zotero/preferences/preferences_export.jsx @@ -35,6 +35,7 @@ Zotero_Preferences.Export = { init: Zotero.Promise.coroutine(function* () { this.updateQuickCopyInstructions(); yield this.populateQuickCopyList(); + yield this.populateNoteQuickCopyList(); var charsetMenu = document.getElementById("zotero-import-charsetMenu"); var charsetMap = Zotero_Charset_Menu.populate(charsetMenu, false); @@ -51,6 +52,8 @@ Zotero_Preferences.Export = { var collation = Zotero.getLocaleCollation(); return collation.compareString(1, a.label, b.label); }); + // Exclude note export translators + translators = translators.filter(x => !x.configOptions || !x.configOptions.noteTranslator); return translators; }, @@ -82,6 +85,80 @@ Zotero_Preferences.Export = { }), + /* + * Builds the note Quick Copy drop-down from the current global pref + */ + populateNoteQuickCopyList: async function () { + // Initialize default format drop-down + var format = Zotero.Prefs.get("export.noteQuickCopy.setting"); + format = Zotero.QuickCopy.unserializeSetting(format); + var menulist = document.getElementById("zotero-noteQuickCopy-menu"); + menulist.setAttribute('preference', "pref-noteQuickCopy-setting"); + + if (!format) { + format = menulist.value; + } + + format = Zotero.QuickCopy.unserializeSetting(format); + + menulist.selectedItem = null; + menulist.removeAllItems(); + + var popup = document.createElement('menupopup'); + menulist.appendChild(popup); + + // add export formats to list + var translation = new Zotero.Translate("export"); + var translators = await translation.getTranslators(); + + translators.sort((a, b) => a.label.localeCompare(b.label)); + + // Remove "Note" prefix from Note HTML translator + let htmlTranslator = translators.find(x => x.translatorID == '897a81c2-9f60-4bec-ae6b-85a5030b8be5'); + if (htmlTranslator) { + htmlTranslator.label = 'HTML'; + } + + // Make sure virtual "Markdown + Rich Text" translator doesn't actually exist + translators = translators.filter(x => x.translatorID != 'a45eca67-1ee8-45e5-b4c6-23fb8a852873'); + + let markdownTranslatorIdx = translators.findIndex(x => x.translatorID == '154c2785-ec83-4c27-8a8a-d27b3a2eded1'); + // Make sure we actually have both translators + if (markdownTranslatorIdx != -1 && htmlTranslator) { + // Exclude standalone Note Markdown translator + translators.splice(markdownTranslatorIdx, 1); + // Add virtual "Markdown + Rich Text" translator to the top + translators.unshift({ + translatorID: 'a45eca67-1ee8-45e5-b4c6-23fb8a852873', + label: 'Markdown + ' + Zotero.getString('general.richText'), + configOptions: { + noteTranslator: true + } + }); + } + + translators.forEach(function (translator) { + // Allow only note export translators + if (!translator.configOptions || !translator.configOptions.noteTranslator) { + return; + } + + var val = JSON.stringify({ mode: 'export', id: translator.translatorID }); + var itemNode = document.createElement('menuitem'); + itemNode.setAttribute('value', val); + itemNode.setAttribute('label', translator.label); + // itemNode.setAttribute('oncommand', 'Zotero_Preferences.Export.updateQuickCopyUI()'); + popup.appendChild(itemNode); + + if (format.mode == 'export' && format.id == translator.translatorID) { + menulist.selectedItem = itemNode; + } + }); + + menulist.click(); + }, + + /* * Builds a Quick Copy drop-down */ diff --git a/chrome/content/zotero/preferences/preferences_export.xul b/chrome/content/zotero/preferences/preferences_export.xul index 8881e864fc..c8b7e4ad0c 100644 --- a/chrome/content/zotero/preferences/preferences_export.xul +++ b/chrome/content/zotero/preferences/preferences_export.xul @@ -41,6 +41,7 @@ + @@ -53,7 +54,7 @@ -