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 @@
+
\ 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,
};
/**