Validate CSL styles on installation, and restructure Zotero.Styles.install() to use Q.
Closes https://www.zotero.org/trac/ticket/1681, automatic CSL validation
This commit is contained in:
parent
bf4c5c1158
commit
06825c4767
3 changed files with 210 additions and 166 deletions
|
@ -30,7 +30,10 @@
|
||||||
*/
|
*/
|
||||||
Zotero.Styles = new function() {
|
Zotero.Styles = new function() {
|
||||||
var _initialized = false;
|
var _initialized = false;
|
||||||
var _styles, _visibleStyles;
|
var _styles, _visibleStyles, _cacheTranslatorData;
|
||||||
|
|
||||||
|
Components.utils.import("resource://zotero/q.jsm");
|
||||||
|
Components.utils.import("resource://gre/modules/Services.jsm");
|
||||||
|
|
||||||
this.xsltProcessor = null;
|
this.xsltProcessor = null;
|
||||||
this.ios = Components.classes["@mozilla.org/network/io-service;1"].
|
this.ios = Components.classes["@mozilla.org/network/io-service;1"].
|
||||||
|
@ -39,6 +42,7 @@ Zotero.Styles = new function() {
|
||||||
this.ns = {
|
this.ns = {
|
||||||
"csl":"http://purl.org/net/xbiblio/csl"
|
"csl":"http://purl.org/net/xbiblio/csl"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes styles cache, loading metadata for styles into memory
|
* Initializes styles cache, loading metadata for styles into memory
|
||||||
|
@ -50,7 +54,7 @@ Zotero.Styles = new function() {
|
||||||
|
|
||||||
_styles = {};
|
_styles = {};
|
||||||
_visibleStyles = [];
|
_visibleStyles = [];
|
||||||
this.cacheTranslatorData = Zotero.Prefs.get("cacheTranslatorData");
|
_cacheTranslatorData = Zotero.Prefs.get("cacheTranslatorData");
|
||||||
this.lastCSL = null;
|
this.lastCSL = null;
|
||||||
|
|
||||||
// main dir
|
// main dir
|
||||||
|
@ -134,7 +138,7 @@ Zotero.Styles = new function() {
|
||||||
* @return {Zotero.Style[]} An array of Zotero.Style objects
|
* @return {Zotero.Style[]} An array of Zotero.Style objects
|
||||||
*/
|
*/
|
||||||
this.getVisible = function() {
|
this.getVisible = function() {
|
||||||
if(!_initialized || !this.cacheTranslatorData) this.init();
|
if(!_initialized || !_cacheTranslatorData) this.init();
|
||||||
return _visibleStyles.slice(0);
|
return _visibleStyles.slice(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,198 +147,234 @@ Zotero.Styles = new function() {
|
||||||
* @return {Object} An object whose keys are style IDs, and whose values are Zotero.Style objects
|
* @return {Object} An object whose keys are style IDs, and whose values are Zotero.Style objects
|
||||||
*/
|
*/
|
||||||
this.getAll = function() {
|
this.getAll = function() {
|
||||||
if(!_initialized || !this.cacheTranslatorData) this.init();
|
if(!_initialized || !_cacheTranslatorData) this.init();
|
||||||
return _styles;
|
return _styles;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Installs a style file
|
* Validates a style
|
||||||
* @param {String|nsIFile} style An nsIFile representing a style on disk, or a string containing
|
* @param {String} style The style, as a string
|
||||||
* the style data
|
* @return {Promise} A promise representing the style file. This promise is rejected
|
||||||
* @param {String} loadURI The URI this style file was loaded from
|
* with the validation error if validation fails, or resolved if it is not.
|
||||||
* @param {Boolean} hidden Whether style is to be hidden. If this parameter is true, UI alerts
|
|
||||||
* are silenced as well
|
|
||||||
*/
|
*/
|
||||||
this.install = function(style, loadURI, hidden) {
|
this.validate = function(style) {
|
||||||
const pathRe = /[^\/]+$/;
|
var deferred = Q.defer(),
|
||||||
|
worker = new Worker("resource://zotero/csl-validator.js");
|
||||||
if(!_initialized || !this.cacheTranslatorData) this.init();
|
worker.onmessage = function(event) {
|
||||||
|
if(event.data) {
|
||||||
// handle nsIFiles
|
deferred.reject(event.data);
|
||||||
var styleFile = null;
|
} else {
|
||||||
|
deferred.resolve();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
worker.postMessage(style);
|
||||||
|
return deferred.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Installs a style file, getting the contents of an nsIFile and showing appropriate
|
||||||
|
* error messages
|
||||||
|
* @param {String|nsIFile} style An nsIFile representing a style on disk, or a string
|
||||||
|
* containing the style data
|
||||||
|
* @param {String} origin The origin of the style, either a filename or URL, to be
|
||||||
|
* displayed in dialogs referencing the style
|
||||||
|
*/
|
||||||
|
this.install = function(style, origin) {
|
||||||
|
var styleFile = null, styleInstalled;
|
||||||
if(style instanceof Components.interfaces.nsIFile) {
|
if(style instanceof Components.interfaces.nsIFile) {
|
||||||
styleFile = style;
|
// handle nsIFiles
|
||||||
loadURI = style.leafName;
|
origin = style.leafName;
|
||||||
style = Zotero.File.getContents(styleFile);
|
styleInstalled = Zotero.File.getContentsAsync(styleFile).when(function(style) {
|
||||||
|
return _install(style, origin);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
styleInstalled = _install(style, origin);
|
||||||
}
|
}
|
||||||
|
|
||||||
var error = false;
|
styleInstalled.fail(function(error) {
|
||||||
try {
|
// Unless user cancelled, show an alert with the error
|
||||||
// CSL
|
if(error instanceof Zotero.Exception.UserCancelled) return;
|
||||||
|
if(error instanceof Zotero.Exception.Alert) {
|
||||||
|
error.present();
|
||||||
|
error.log();
|
||||||
|
} else {
|
||||||
|
Zotero.logError(error);
|
||||||
|
(new Zotero.Exception.Alert("styles.install.unexpectedError",
|
||||||
|
origin, "styles.install.title", error)).present();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Installs a style
|
||||||
|
* @param {String} style The style as a string
|
||||||
|
* @param {String} origin The origin of the style, either a filename or URL, to be
|
||||||
|
* displayed in dialogs referencing the style
|
||||||
|
* @param {Boolean} [hidden] Whether style is to be hidden.
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
function _install(style, origin, hidden) {
|
||||||
|
if(!_initialized || !_cacheTranslatorData) Zotero.Styles.init();
|
||||||
|
|
||||||
|
var existingFile, destFile, source;
|
||||||
|
return Q.fcall(function() {
|
||||||
|
// First, parse style and make sure it's valid XML
|
||||||
var parser = Components.classes["@mozilla.org/xmlextras/domparser;1"]
|
var parser = Components.classes["@mozilla.org/xmlextras/domparser;1"]
|
||||||
.createInstance(Components.interfaces.nsIDOMParser),
|
.createInstance(Components.interfaces.nsIDOMParser),
|
||||||
doc = parser.parseFromString(style, "application/xml");
|
doc = parser.parseFromString(style, "application/xml");
|
||||||
if(doc.documentElement.localName === "parsererror") {
|
|
||||||
throw new Error("File is not valid XML");
|
var styleID = Zotero.Utilities.xpathText(doc, '/csl:style/csl:info[1]/csl:id[1]',
|
||||||
|
Zotero.Styles.ns),
|
||||||
|
// Get file name from URL
|
||||||
|
m = /[^\/]+$/.exec(styleID),
|
||||||
|
fileName = Zotero.File.getValidFileName(m ? m[0] : styleID),
|
||||||
|
title = Zotero.Utilities.xpathText(doc, '/csl:style/csl:info[1]/csl:title[1]',
|
||||||
|
Zotero.Styles.ns);
|
||||||
|
|
||||||
|
if(!styleID || !title) {
|
||||||
|
// If it's not valid XML, we'll return a promise that immediately resolves
|
||||||
|
// to an error
|
||||||
|
throw new Zotero.Exception.Alert("styles.installError", origin,
|
||||||
|
"styles.install.title", "Style is not valid XML, or the styleID or title is missing");
|
||||||
}
|
}
|
||||||
} catch(e) {
|
|
||||||
error = e;
|
// look for a parent
|
||||||
}
|
source = Zotero.Utilities.xpathText(doc,
|
||||||
|
'/csl:style/csl:info[1]/csl:link[@rel="source" or @rel="independent-parent"][1]/@href',
|
||||||
if(!doc || error) {
|
Zotero.Styles.ns);
|
||||||
if(!hidden) alert(Zotero.getString('styles.installError', loadURI));
|
if(source == styleID) {
|
||||||
if(error) throw error;
|
throw Zotero.Exception.Alert("styles.installError", origin,
|
||||||
return false;
|
"styles.install.title", "Style references itself as source");
|
||||||
}
|
|
||||||
|
|
||||||
var styleID = Zotero.Utilities.xpathText(doc, '/csl:style/csl:info[1]/csl:id[1]',
|
|
||||||
Zotero.Styles.ns);
|
|
||||||
// get file name from URL
|
|
||||||
var m = pathRe.exec(styleID);
|
|
||||||
var fileName = Zotero.File.getValidFileName(m ? m[0] : styleID);
|
|
||||||
var title = Zotero.Utilities.xpathText(doc, '/csl:style/csl:info[1]/csl:title[1]',
|
|
||||||
Zotero.Styles.ns);
|
|
||||||
|
|
||||||
// look for a parent
|
|
||||||
var source = Zotero.Utilities.xpathText(doc,
|
|
||||||
'/csl:style/csl:info[1]/csl:link[@rel="source" or @rel="independent-parent"][1]/@href',
|
|
||||||
Zotero.Styles.ns);
|
|
||||||
if(source == styleID) {
|
|
||||||
if(!hidden) alert(Zotero.getString('styles.installError', loadURI));
|
|
||||||
throw "Style with ID "+styleID+" references itself as source";
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensure csl extension
|
|
||||||
if(fileName.substr(-4).toLowerCase() != ".csl") fileName += ".csl";
|
|
||||||
|
|
||||||
var destFile = Zotero.getStylesDirectory();
|
|
||||||
var destFileHidden = destFile.clone();
|
|
||||||
destFile.append(fileName);
|
|
||||||
destFileHidden.append("hidden");
|
|
||||||
if(hidden) Zotero.File.createDirectoryIfMissing(destFileHidden);
|
|
||||||
destFileHidden.append(fileName);
|
|
||||||
|
|
||||||
// look for an existing style with the same styleID or filename
|
|
||||||
var existingFile = null;
|
|
||||||
var existingTitle = null;
|
|
||||||
if(_styles[styleID]) {
|
|
||||||
existingFile = _styles[styleID].file;
|
|
||||||
existingTitle = _styles[styleID].title;
|
|
||||||
} else {
|
|
||||||
if(destFile.exists()) {
|
|
||||||
existingFile = destFile;
|
|
||||||
} else if(destFileHidden.exists()) {
|
|
||||||
existingFile = destFileHidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(existingFile) {
|
// ensure csl extension
|
||||||
// find associated style
|
if(fileName.substr(-4).toLowerCase() != ".csl") fileName += ".csl";
|
||||||
for each(var existingStyle in this._styles) {
|
|
||||||
if(destFile.equals(existingStyle.file)) {
|
destFile = Zotero.getStylesDirectory();
|
||||||
|
var destFileHidden = destFile.clone();
|
||||||
|
destFile.append(fileName);
|
||||||
|
destFileHidden.append("hidden");
|
||||||
|
if(hidden) Zotero.File.createDirectoryIfMissing(destFileHidden);
|
||||||
|
destFileHidden.append(fileName);
|
||||||
|
|
||||||
|
// look for an existing style with the same styleID or filename
|
||||||
|
var existingTitle;
|
||||||
|
if(_styles[styleID]) {
|
||||||
|
existingFile = _styles[styleID].file;
|
||||||
|
existingTitle = _styles[styleID].title;
|
||||||
|
} else {
|
||||||
|
if(destFile.exists()) {
|
||||||
|
existingFile = destFile;
|
||||||
|
} else if(destFileHidden.exists()) {
|
||||||
|
existingFile = destFileHidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(existingFile) {
|
||||||
|
// find associated style
|
||||||
|
for each(var existingStyle in _styles) {
|
||||||
|
if(destFile.equals(existingStyle.file)) {
|
||||||
|
existingTitle = existingStyle.title;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// also look for an existing style with the same title
|
||||||
|
if(!existingFile) {
|
||||||
|
for each(var existingStyle in Zotero.Styles.getAll()) {
|
||||||
|
if(title === existingStyle.title) {
|
||||||
|
existingFile = existingStyle.file;
|
||||||
existingTitle = existingStyle.title;
|
existingTitle = existingStyle.title;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// display a dialog to tell the user we're about to install the style
|
||||||
// also look for an existing style with the same title
|
if(hidden) {
|
||||||
if(!existingFile) {
|
destFile = destFileHidden;
|
||||||
for each(var existingStyle in this.getAll()) {
|
} else {
|
||||||
if(title === existingStyle.title) {
|
if(existingTitle) {
|
||||||
existingFile = existingStyle.file;
|
var text = Zotero.getString('styles.updateStyle', [existingTitle, title, origin]);
|
||||||
existingTitle = existingStyle.title;
|
} else {
|
||||||
break;
|
var text = Zotero.getString('styles.installStyle', [title, origin]);
|
||||||
|
}
|
||||||
|
|
||||||
|
var index = Services.prompt.confirmEx(null, Zotero.getString('styles.install.title'),
|
||||||
|
text,
|
||||||
|
((Services.prompt.BUTTON_POS_0) * (Services.prompt.BUTTON_TITLE_IS_STRING)
|
||||||
|
+ (Services.prompt.BUTTON_POS_1) * (Services.prompt.BUTTON_TITLE_CANCEL)),
|
||||||
|
Zotero.getString('general.install'), null, null, null, {}
|
||||||
|
);
|
||||||
|
|
||||||
|
if(index !== 0) {
|
||||||
|
throw new Zotero.Exception.UserCancelled("style installation");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// display a dialog to tell the user we're about to install the style
|
return Zotero.Styles.validate(style).fail(function(validationErrors) {
|
||||||
if(hidden) {
|
Zotero.logError("Style from "+origin+" failed to validate:\n\n"+validationErrors);
|
||||||
destFile = destFileHidden;
|
|
||||||
} else {
|
// If validation fails on the parent of a dependent style, ignore it (for now)
|
||||||
var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
|
if(hidden) return;
|
||||||
.getService(Components.interfaces.nsIPromptService);
|
|
||||||
|
// If validation fails on a different style, we ask the user if s/he really
|
||||||
if(existingTitle) {
|
// wants to install it
|
||||||
var text = Zotero.getString('styles.updateStyle', [existingTitle, title, loadURI]);
|
Components.utils.import("resource://gre/modules/Services.jsm");
|
||||||
} else {
|
var shouldInstall = Services.prompt.confirmEx(null,
|
||||||
var text = Zotero.getString('styles.installStyle', [title, loadURI]);
|
Zotero.getString('styles.install.title'),
|
||||||
}
|
Zotero.getString('styles.validationWarning', origin),
|
||||||
|
(Services.prompt.BUTTON_POS_0) * (Services.prompt.BUTTON_TITLE_OK)
|
||||||
var index = ps.confirmEx(null, '',
|
+ (Services.prompt.BUTTON_POS_1) * (Services.prompt.BUTTON_TITLE_CANCEL)
|
||||||
text,
|
+ Services.prompt.BUTTON_POS_1_DEFAULT + Services.prompt.BUTTON_DELAY_ENABLE,
|
||||||
((ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING)
|
null, null, null, null, {}
|
||||||
+ (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_CANCEL)),
|
);
|
||||||
Zotero.getString('general.install'), null, null, null, {}
|
if(shouldInstall !== 0) {
|
||||||
);
|
throw new Zotero.Exception.UserCancelled("style installation");
|
||||||
}
|
}
|
||||||
|
});
|
||||||
if(hidden || index == 0) {
|
}).then(function() {
|
||||||
// user wants to install/update
|
// User wants to install/update
|
||||||
if(source && !_styles[source]) {
|
if(source && !_styles[source]) {
|
||||||
// need to fetch source
|
// Need to fetch source
|
||||||
if(source.substr(0, 7) == "http://" || source.substr(0, 8) == "https://") {
|
if(source.substr(0, 7) === "http://" || source.substr(0, 8) === "https://") {
|
||||||
Zotero.HTTP.doGet(source, function(xmlhttp) {
|
return Zotero.HTTP.promise("GET", source).then(function(xmlhttp) {
|
||||||
var success = false;
|
return _install(xmlhttp.responseText, origin, true);
|
||||||
var error = null;
|
}).fail(function(error) {
|
||||||
try {
|
if(error instanceof Zotero.Exception) {
|
||||||
var success = Zotero.Styles.install(xmlhttp.responseText, loadURI, true);
|
throw new Zotero.Exception.Alert("styles.installSourceError", [origin, source],
|
||||||
} catch(e) {
|
"styles.install.title", error);
|
||||||
error = e;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(success) {
|
|
||||||
_completeInstall(style, styleID, destFile, existingFile, styleFile);
|
|
||||||
} else {
|
} else {
|
||||||
if(!hidden) alert(Zotero.getString('styles.installSourceError', [loadURI, source]));
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if(!hidden) alert(Zotero.getString('styles.installSourceError', [loadURI, source]));
|
throw new Zotero.Exception.Alert("styles.installSourceError", [origin, source],
|
||||||
throw "Source CSL URI is invalid";
|
"styles.install.title", "Source CSL URI is invalid");
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
_completeInstall(style, styleID, destFile, existingFile, styleFile);
|
|
||||||
}
|
}
|
||||||
return styleID;
|
}).then(function() {
|
||||||
}
|
// Dependent style has been retrieved if there was one, so we're ready to
|
||||||
|
// continue
|
||||||
return false;
|
|
||||||
}
|
// Remove any existing file with a different name
|
||||||
|
if(existingFile) existingFile.remove(false);
|
||||||
/**
|
|
||||||
* Finishes installing a style, copying the file, reloading the style cache, and refreshing the
|
return Zotero.File.putContentsAsync(destFile, style);
|
||||||
* styles list in any open windows
|
}).then(function() {
|
||||||
* @param {String} style The style string
|
// Cache
|
||||||
* @param {String} styleID The style ID
|
Zotero.Styles.init();
|
||||||
* @param {nsIFile} destFile The destination for the style
|
|
||||||
* @param {nsIFile} [existingFile] The existing file to delete before copying this one
|
// Refresh preferences windows
|
||||||
* @param {nsIFile} [styleFile] The file that contains the style to be installed
|
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"].
|
||||||
* @private
|
getService(Components.interfaces.nsIWindowMediator);
|
||||||
*/
|
var enumerator = wm.getEnumerator("zotero:pref");
|
||||||
function _completeInstall(style, styleID, destFile, existingFile, styleFile) {
|
while(enumerator.hasMoreElements()) {
|
||||||
// remove any existing file with a different name
|
var win = enumerator.getNext();
|
||||||
if(existingFile) existingFile.remove(false);
|
win.refreshStylesList(styleID);
|
||||||
|
}
|
||||||
if(styleFile) {
|
});
|
||||||
styleFile.copyToFollowingLinks(destFile.parent, destFile.leafName);
|
|
||||||
} else {
|
|
||||||
Zotero.File.putContents(destFile, style);
|
|
||||||
}
|
|
||||||
|
|
||||||
// recache
|
|
||||||
Zotero.Styles.init();
|
|
||||||
|
|
||||||
// refresh preferences windows
|
|
||||||
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"].
|
|
||||||
getService(Components.interfaces.nsIWindowMediator);
|
|
||||||
var enumerator = wm.getEnumerator("zotero:pref");
|
|
||||||
while(enumerator.hasMoreElements()) {
|
|
||||||
var win = enumerator.getNext();
|
|
||||||
win.refreshStylesList(styleID);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -638,10 +638,13 @@ integration.citationChanged = You have modified this citation since Zotero ge
|
||||||
integration.citationChanged.description = Clicking "Yes" will prevent Zotero from updating this citation if you add additional citations, switch styles, or modify the item to which it refers. Clicking "No" will erase your changes.
|
integration.citationChanged.description = Clicking "Yes" will prevent Zotero from updating this citation if you add additional citations, switch styles, or modify the item to which it refers. Clicking "No" will erase your changes.
|
||||||
integration.citationChanged.edit = You have modified this citation since Zotero generated it. Editing will clear your modifications. Do you want to continue?
|
integration.citationChanged.edit = You have modified this citation since Zotero generated it. Editing will clear your modifications. Do you want to continue?
|
||||||
|
|
||||||
|
styles.install.title = Install Style
|
||||||
|
styles.install.unexpectedError = An unexpected error occurred while installing "%1$S"
|
||||||
styles.installStyle = Install style "%1$S" from %2$S?
|
styles.installStyle = Install style "%1$S" from %2$S?
|
||||||
styles.updateStyle = Update existing style "%1$S" with "%2$S" from %3$S?
|
styles.updateStyle = Update existing style "%1$S" with "%2$S" from %3$S?
|
||||||
styles.installed = The style "%S" was installed successfully.
|
styles.installed = The style "%S" was installed successfully.
|
||||||
styles.installError = %S does not appear to be a valid style file.
|
styles.installError = "%S" is not a valid style file.
|
||||||
|
styles.validationWarning = "%S" is not valid CSL 1.0, and may not work properly with Zotero.\n\nAre you sure you want to continue?
|
||||||
styles.installSourceError = %1$S references an invalid or non-existent CSL file at %2$S as its source.
|
styles.installSourceError = %1$S references an invalid or non-existent CSL file at %2$S as its source.
|
||||||
styles.deleteStyle = Are you sure you want to delete the style "%1$S"?
|
styles.deleteStyle = Are you sure you want to delete the style "%1$S"?
|
||||||
styles.deleteStyles = Are you sure you want to delete the selected styles?
|
styles.deleteStyles = Are you sure you want to delete the selected styles?
|
||||||
|
|
1
resource/csl-validator.js
Normal file
1
resource/csl-validator.js
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue