diff --git a/chrome/content/zotero-platform/mac/bibliography.css b/chrome/content/zotero-platform/mac/bibliography.css new file mode 100644 index 0000000000..9a476e2b61 --- /dev/null +++ b/chrome/content/zotero-platform/mac/bibliography.css @@ -0,0 +1,3 @@ +.chevron { + font-size: 1.5em; +} \ No newline at end of file diff --git a/chrome/content/zotero/bibliography.js b/chrome/content/zotero/bibliography.js index b5356b0070..cea8610800 100644 --- a/chrome/content/zotero/bibliography.js +++ b/chrome/content/zotero/bibliography.js @@ -32,6 +32,7 @@ // Class to provide options for bibliography // Used by rtfScan.xul, integrationDocPrefs.xul, and bibliography.xul +Components.utils.import("resource://gre/modules/Services.jsm"); var Zotero_File_Interface_Bibliography = new function() { var _io; @@ -174,6 +175,10 @@ var Zotero_File_Interface_Bibliography = new function() { document.getElementById("automaticCitationUpdates-checkbox").checked = !_io.delayCitationUpdates; } + + if (_io.showImportExport) { + document.querySelector('#exportImport').hidden = false; + } } // set style to false, in case this is cancelled @@ -237,7 +242,38 @@ var Zotero_File_Interface_Bibliography = new function() { window.sizeToContent(); }; - + + this.toggleAdvanced = function() { + var advancedSettings = document.querySelector("#advanced-settings"); + advancedSettings.hidden = !advancedSettings.hidden; + var chevron = document.querySelector('.chevron'); + chevron.classList.toggle('chevron-down'); + chevron.classList.toggle('chevron-up'); + window.sizeToContent(); + }; + + this.exportDocument = function() { + const importExportWikiURL = "https://www.zotero.org/support/kb/export_import_document"; + + var ps = Services.prompt; + var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_OK) + + (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_CANCEL) + + (ps.BUTTON_POS_2) * (ps.BUTTON_TITLE_IS_STRING); + var result = ps.confirmEx(null, + Zotero.getString('integration.exportDocument'), + Zotero.getString('integration.exportDocument.description'), + buttonFlags, + null, + null, + Zotero.getString('general.moreInformation'), null, {}); + if (result == 0) { + _io.exportDocument = true; + document.documentElement.acceptDialog(); + } else if (result == 2) { + Zotero.launchURL(importExportWikiURL); + } + } + /* * Update locale menulist when style is changed */ diff --git a/chrome/content/zotero/integration/integrationDocPrefs.xul b/chrome/content/zotero/integration/integrationDocPrefs.xul index f438c2bee3..c6c069b0a9 100644 --- a/chrome/content/zotero/integration/integrationDocPrefs.xul +++ b/chrome/content/zotero/integration/integrationDocPrefs.xul @@ -26,6 +26,7 @@ + - + &zotero.integration.prefs.automaticJournalAbbeviations.caption; - - - &zotero.integration.prefs.automaticCitationUpdates.description; + + + + + + + + \ No newline at end of file diff --git a/chrome/content/zotero/xpcom/connector/httpIntegrationClient.js b/chrome/content/zotero/xpcom/connector/httpIntegrationClient.js index 63f5f69835..83e5725df2 100644 --- a/chrome/content/zotero/xpcom/connector/httpIntegrationClient.js +++ b/chrome/content/zotero/xpcom/connector/httpIntegrationClient.js @@ -59,12 +59,16 @@ Zotero.HTTPIntegrationClient.Application = function() { this.secondaryFieldType = "Http"; this.outputFormat = 'html'; this.supportedNotes = ['footnotes']; + this.supportsImportExport = false; + this.processorName = "HTTP Integration"; }; Zotero.HTTPIntegrationClient.Application.prototype = { getActiveDocument: async function() { let result = await Zotero.HTTPIntegrationClient.sendCommand('Application.getActiveDocument'); this.outputFormat = result.outputFormat || this.outputFormat; this.supportedNotes = result.supportedNotes || this.supportedNotes; + this.supportsImportExport = result.supportsImportExport || this.supportsImportExport; + this.processorName = result.processorName || this.processorName; return new Zotero.HTTPIntegrationClient.Document(result.documentID); } }; @@ -76,7 +80,7 @@ Zotero.HTTPIntegrationClient.Document = function(documentID) { this._documentID = documentID; }; for (let method of ["activate", "canInsertField", "displayAlert", "getDocumentData", - "setDocumentData", "setBibliographyStyle"]) { + "setDocumentData", "setBibliographyStyle", "importDocument", "exportDocument"]) { Zotero.HTTPIntegrationClient.Document.prototype[method] = async function() { return Zotero.HTTPIntegrationClient.sendCommand("Document."+method, [this._documentID].concat(Array.prototype.slice.call(arguments))); diff --git a/chrome/content/zotero/xpcom/connector/server_connectorIntegration.js b/chrome/content/zotero/xpcom/connector/server_connectorIntegration.js index ebe9e18206..24c4642f65 100644 --- a/chrome/content/zotero/xpcom/connector/server_connectorIntegration.js +++ b/chrome/content/zotero/xpcom/connector/server_connectorIntegration.js @@ -74,7 +74,7 @@ Zotero.Server.Endpoints['/connector/document/respond'].prototype = { // For managing macOS integration and progress window focus Zotero.Server.Endpoints['/connector/sendToBack'] = function() {}; Zotero.Server.Endpoints['/connector/sendToBack'].prototype = { - supportedMethods: ["POST"], + supportedMethods: ["POST", "GET"], supportedDataTypes: ["application/json"], permitBookmarklet: true, init: function() { diff --git a/chrome/content/zotero/xpcom/integration.js b/chrome/content/zotero/xpcom/integration.js index 3d8a8a8144..90add4f459 100644 --- a/chrome/content/zotero/xpcom/integration.js +++ b/chrome/content/zotero/xpcom/integration.js @@ -59,6 +59,8 @@ const DELAYED_CITATION_RTF_STYLING_CLEAR = "\\ulclear"; const DELAYED_CITATION_HTML_STYLING = "
" const DELAYED_CITATION_HTML_STYLING_END = "
" +const EXPORTED_DOCUMENT_MARKER = "ZOTERO_EXPORTED_DOCUMENT"; + Zotero.Integration = new function() { Components.utils.import("resource://gre/modules/Services.jsm"); @@ -210,7 +212,7 @@ Zotero.Integration = new function() { * Executes an integration command, first checking to make sure that versions are compatible */ this.execCommand = async function(agent, command, docId) { - var document, session; + var document, session, documentImported; if (Zotero.Integration.currentDoc) { Zotero.Utilities.Internal.activate(); @@ -243,12 +245,13 @@ Zotero.Integration = new function() { } Zotero.Integration.currentDoc = document = await documentPromise; - Zotero.Integration.currentSession = session = await Zotero.Integration.getSession(application, document, agent); - // TODO: this is a pretty awful circular dependence - session.fields = new Zotero.Integration.Fields(session, document); + [session, documentImported] = await Zotero.Integration.getSession(application, document, agent); + Zotero.Integration.currentSession = session; // TODO: figure this out // Zotero.Notifier.trigger('delete', 'collection', 'document'); - await (new Zotero.Integration.Interface(application, document, session))[command](); + if (!documentImported) { + await (new Zotero.Integration.Interface(application, document, session))[command](); + } await document.setDocumentData(session.data.serialize()); } catch (e) { @@ -257,7 +260,9 @@ Zotero.Integration = new function() { } else { // If user cancels we should still write the currently assigned session ID - await document.setDocumentData(session.data.serialize()); + if (session) { + await document.setDocumentData(session.data.serialize()); + } } } finally { @@ -415,6 +420,7 @@ Zotero.Integration = new function() { * @return {Zotero.Integration.Session} Promise */ this.getSession = async function (app, doc, agent) { + let documentImported = false; try { var progressBar = new Zotero.Integration.Progress(4, Zotero.isMac && agent != 'http'); progressBar.show(); @@ -428,7 +434,7 @@ Zotero.Integration = new function() { data = new Zotero.Integration.DocumentData(); } - if (data.prefs.fieldType) { + if (dataString != EXPORTED_DOCUMENT_MARKER && data.prefs.fieldType) { if (data.dataVersion < DATA_VERSION) { if (data.dataVersion == 1 && data.prefs.fieldType == "Field" @@ -462,36 +468,54 @@ Zotero.Integration = new function() { session = new Zotero.Integration.Session(doc, app); session.reload = true; } - try { - await session.setData(data); - } catch(e) { - // make sure style is defined - if (e instanceof Zotero.Exception.Alert && e.name === "integration.error.invalidStyle") { - if (data.style.styleID) { - let trustedSource = /^https?:\/\/(www\.)?(zotero\.org|citationstyles\.org)/.test(data.style.styleID); - let errorString = Zotero.getString("integration.error.styleMissing", data.style.styleID); - if (trustedSource || - await doc.displayAlert(errorString, DIALOG_ICON_WARNING, DIALOG_BUTTONS_YES_NO)) { + session.agent = agent; + session._doc = doc; + session.progressBar = progressBar; + // TODO: this is a pretty awful circular dependence + session.fields = new Zotero.Integration.Fields(session, doc); + + if (dataString == EXPORTED_DOCUMENT_MARKER) { + Zotero.Integration.currentSession = session; + data = await session.importDocument(); + documentImported = true; + // We're slightly abusing the system here, but importing a document should cancel + // any other operation the user was trying to perform since the document will change + // significantly + } else { + try { + await session.setData(data); + } catch(e) { + // make sure style is defined + if (e instanceof Zotero.Exception.Alert && e.name === "integration.error.invalidStyle") { + if (data.style.styleID) { + let trustedSource = + /^https?:\/\/(www\.)?(zotero\.org|citationstyles\.org)/.test(data.style.styleID); + let errorString = Zotero.getString("integration.error.styleMissing", data.style.styleID); + if (trustedSource || + await doc.displayAlert(errorString, DIALOG_ICON_WARNING, DIALOG_BUTTONS_YES_NO)) { - let installed = false; - try { - let { styleTitle, styleID } = await Zotero.Styles.install( - {url: data.style.styleID}, data.style.styleID, true - ); - data.style.styleID = styleID; - installed = true; - } - catch (e) { - await doc.displayAlert( - Zotero.getString( - 'integration.error.styleNotFound', data.style.styleID - ), - DIALOG_ICON_WARNING, - DIALOG_BUTTONS_OK - ); - } - if (installed) { - await session.setData(data, true); + let installed = false; + try { + let { styleTitle, styleID } = await Zotero.Styles.install( + {url: data.style.styleID}, data.style.styleID, true + ); + data.style.styleID = styleID; + installed = true; + } + catch (e) { + await doc.displayAlert( + Zotero.getString( + 'integration.error.styleNotFound', data.style.styleID + ), + DIALOG_ICON_WARNING, + DIALOG_BUTTONS_OK + ); + } + if (installed) { + await session.setData(data, true); + } else { + await session.setDocPrefs(); + } } else { await session.setDocPrefs(); } @@ -499,24 +523,19 @@ Zotero.Integration = new function() { await session.setDocPrefs(); } } else { - await session.setDocPrefs(); + throw e; } - } else { - throw e; } } } catch (e) { progressBar.hide(true); throw e; } - session.agent = agent; - session._doc = doc; if (session.progressBar) { progressBar.reset(); progressBar.segments = session.progressBar.segments; } - session.progressBar = progressBar; - return session; + return [session, documentImported]; }; } @@ -731,12 +750,12 @@ Zotero.Integration.Interface.prototype.setDocPrefs = Zotero.Promise.coroutine(fu if(!haveSession) { // This is a brand new document; don't try to get fields - oldData = yield this._session.setDocPrefs(); + oldData = yield this._session.setDocPrefs(true); } else { // Can get fields while dialog is open oldData = yield Zotero.Promise.all([ this._session.fields.get(), - this._session.setDocPrefs() + this._session.setDocPrefs(true) ]).spread(function (fields, setDocPrefs) { // Only return value from setDocPrefs return setDocPrefs; @@ -861,9 +880,9 @@ Zotero.Integration.Fields.prototype.addField = async function(note) { */ Zotero.Integration.Fields.prototype.get = new function() { var deferred; - return async function() { + return async function(force=false) { // If we already have fields, just return them - if(this._fields != undefined) { + if(!force && this._fields != undefined) { return this._fields; } @@ -1519,10 +1538,11 @@ Zotero.Integration.Session.prototype.setData = async function (data, resetStyle) * if there wasn't, or rejected with Zotero.Exception.UserCancelled if the dialog was * cancelled. */ -Zotero.Integration.Session.prototype.setDocPrefs = Zotero.Promise.coroutine(function* (highlightDelayCitations=false) { +Zotero.Integration.Session.prototype.setDocPrefs = Zotero.Promise.coroutine(function* (showImportExport=false) { var io = new function() { this.wrappedJSObject = this; }; io.primaryFieldType = this.primaryFieldType; io.secondaryFieldType = this.secondaryFieldType; + io.showImportExport = false; if (this.data) { io.style = this.data.style.styleID; @@ -1532,14 +1552,18 @@ Zotero.Integration.Session.prototype.setDocPrefs = Zotero.Promise.coroutine(func io.fieldType = this.data.prefs.fieldType; io.delayCitationUpdates = this.data.prefs.delayCitationUpdates; io.dontAskDelayCitationUpdates = this.data.prefs.dontAskDelayCitationUpdates; - io.highlightDelayCitations = highlightDelayCitations; io.automaticJournalAbbreviations = this.data.prefs.automaticJournalAbbreviations; io.requireStoreReferences = !Zotero.Utilities.isEmpty(this.embeddedItems); + io.showImportExport = showImportExport && this.data.prefs.fieldType && this._app.supportsImportExport; } // Make sure styles are initialized for new docs yield Zotero.Styles.init(); yield Zotero.Integration.displayDialog('chrome://zotero/content/integration/integrationDocPrefs.xul', '', io); + + if (io.exportDocument) { + return this.exportDocument(); + } if (!io.style || !io.fieldType) { throw new Zotero.Exception.UserCancelled("document preferences window"); @@ -1581,14 +1605,61 @@ Zotero.Integration.Session.prototype.setDocPrefs = Zotero.Promise.coroutine(func return oldData || null; }) -/** - * Adds a citation based on a serialized Word field - */ -Zotero.Integration._oldCitationLocatorMap = { - p:"page", - g:"paragraph", - l:"line" -}; +Zotero.Integration.Session.prototype.exportDocument = async function() { + Zotero.debug("Integration: Exporting the document"); + var timer = new Zotero.Integration.Timer(); + timer.start(); + try { + this.data.style.bibliographyStyleHasBeenSet = false; + await this._doc.setDocumentData(this.data.serialize()); + await this._doc.exportDocument(this.data.prefs.fieldType, + Zotero.getString('integration.importInstructions')); + } finally { + Zotero.debug(`Integration: Export finished in ${timer.stop()}`); + } +} + + +Zotero.Integration.Session.prototype.importDocument = async function() { + const importExportWikiURL = "https://www.zotero.org/support/kb/export_import_document"; + + var ps = Services.prompt; + var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_OK) + + (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_CANCEL) + + (ps.BUTTON_POS_2) * (ps.BUTTON_TITLE_IS_STRING); + var result = ps.confirmEx(null, + Zotero.getString('integration.importDocument'), + Zotero.getString('integration.importDocument.description'), + buttonFlags, + null, + null, + Zotero.getString('general.moreInformation'), null, {}); + if (result == 1) { + throw new Zotero.Exception.UserCancelled("the document import"); + } else if (result == 2) { + Zotero.launchURL(importExportWikiURL); + return; + } + Zotero.debug("Integration: Importing the document"); + var timer = new Zotero.Integration.Timer(); + timer.start(); + try { + var importSuccessful = await this._doc.importDocument(this._app.primaryFieldType); + if (!importSuccessful) { + Zotero.debug("Integration: No importable data found in the document"); + return this.displayAlert("No importable data found", DIALOG_ICON_WARNING, DIALOG_BUTTONS_OK); + } + var data = new Zotero.Integration.DocumentData(await this._doc.getDocumentData()); + data.prefs.fieldType = this._app.primaryFieldType; + await this.setData(data, true); + await this.fields.get(true); + await this.fields.updateSession(FORCE_CITATIONS_RESET_TEXT); + await this.fields.updateDocument(FORCE_CITATIONS_RESET_TEXT, true, true); + } finally { + Zotero.debug(`Integration: Import finished in ${timer.stop()}`); + } + return data; +} /** * Adds a citation to the arrays representing the document @@ -2228,6 +2299,16 @@ Zotero.Integration.Field.loadExisting = async function(docField) { return field; }; +/** + * Adds a citation based on a serialized Word field + */ +Zotero.Integration._oldCitationLocatorMap = { + p:"page", + g:"paragraph", + l:"line" +}; + + Zotero.Integration.CitationField = class extends Zotero.Integration.Field { constructor(field, rawCode) { super(field, rawCode); @@ -2908,7 +2989,8 @@ Zotero.Integration.LegacyPluginWrapper = function(application) { primaryFieldType: application.primaryFieldType, secondaryFieldType: application.secondaryFieldType, outputFormat: 'rtf', - supportedNotes: ['footnotes', 'endnotes'] + supportedNotes: ['footnotes', 'endnotes'], + processorName: '' } } Zotero.Integration.LegacyPluginWrapper.wrapField = function (field) { diff --git a/chrome/locale/en-US/zotero/zotero.dtd b/chrome/locale/en-US/zotero/zotero.dtd index e215ad5c20..b9983a5885 100644 --- a/chrome/locale/en-US/zotero/zotero.dtd +++ b/chrome/locale/en-US/zotero/zotero.dtd @@ -243,10 +243,12 @@ - + + + diff --git a/chrome/locale/en-US/zotero/zotero.properties b/chrome/locale/en-US/zotero/zotero.properties index 4b501756c5..f12e0ad30e 100644 --- a/chrome/locale/en-US/zotero/zotero.properties +++ b/chrome/locale/en-US/zotero/zotero.properties @@ -897,6 +897,11 @@ integration.delayCitationUpdates.alert.text2.tab = You will need to click Refres integration.delayCitationUpdates.alert.text3 = You can change this setting later in the document preferences. integration.delayCitationUpdates.bibliography.toolbar = Automatic citation updates are disabled. To see the bibliography, click Refresh in the Zotero toolbar. integration.delayCitationUpdates.bibliography.tab = Automatic citation updates are disabled. To see the bibliography, click Refresh in the Zotero tab. +integration.importDocument = Import Document? +integration.importDocument.description = Would you like to import this document for use with Zotero? +integration.exportDocument = Export Document? +integration.exportDocument.description = Exporting the document will allow you to import it in a different Zotero supported word processor and retain citation links. You should make a backup of your document before exporting. Do you want to proceed? +integration.importInstructions = This document contains exported Zotero citations. Open it with a Zotero supported document editor and press "Refresh" in the Zotero plugin to import it. NOTE: Do not copy and paste the contents of the document from one processor to the other. styles.install.title = Install Style styles.install.unexpectedError = An unexpected error occurred while installing "%1$S" diff --git a/chrome/skin/default/zotero/bibliography.css b/chrome/skin/default/zotero/bibliography.css index c78c789777..2352bec77b 100644 --- a/chrome/skin/default/zotero/bibliography.css +++ b/chrome/skin/default/zotero/bibliography.css @@ -25,6 +25,25 @@ radio:not(:first-child) } -#automaticJournalAbbreviations-vbox, #automaticCitationUpdates-vbox { +#automaticJournalAbbreviations-vbox, #advanced-settings { padding: 0 14px; -} \ No newline at end of file +} + +#advanced-separator * { + cursor: pointer; +} + +.chevron { + color: #444; +} + +.chevron.chevron-down { + transform: rotate(90deg); +} +.chevron.chevron-up { + transform: rotate(-90deg); +} + +#advanced-settings > * { + margin-bottom: 10px; +} diff --git a/test/tests/integrationTest.js b/test/tests/integrationTest.js index 4b757dd85c..cf28e3ffd0 100644 --- a/test/tests/integrationTest.js +++ b/test/tests/integrationTest.js @@ -21,6 +21,7 @@ describe("Zotero.Integration", function () { this.primaryFieldType = "Field"; this.secondaryFieldType = "Bookmark"; this.supportedNotes = ['footnotes', 'endnotes']; + this.supportsImportExport = true; this.fields = []; }; DocumentPluginDummy.Application.prototype = { @@ -119,6 +120,31 @@ describe("Zotero.Integration", function () { * Informs the document processor that the operation is complete */ complete: () => 0, + + /** + * Converts field text in document to their underlying codes and appends + * document preferences and bibliography style as paragraphs at the end + * of the document. Prefixes: + * - Bibliography style: "BIBLIOGRAPHY_STYLE " + * - Document preferences: "DOCUMENT_PREFERENCES " + * + * All Zotero exported text must be converted to a hyperlink + * (with any url, e.g. http://www.zotero.org) + */ + exportDocument: (fieldType) => 0, + + /** + * Converts a document from an exported form described in #exportDocument() + * to a Zotero editable form. Bibliography Style and Document Preferences + * text is removed and stored internally within the doc. The citation codes are + * also stored within the doc in appropriate representation. + * + * Note that no citation text updates are needed. Zotero will issue field updates + * manually. + * + * @returns {Boolean} whether the document contained importable data + */ + importDocument: (fieldType) => 0, }; /**