diff --git a/chrome/content/zotero/xpcom/intl.js b/chrome/content/zotero/xpcom/intl.js index 783891e4c7..d13ef8218c 100644 --- a/chrome/content/zotero/xpcom/intl.js +++ b/chrome/content/zotero/xpcom/intl.js @@ -24,6 +24,7 @@ */ Zotero.Intl = new function () { let bundle; + let collation; let intlProps; let pluralFormGet; let pluralFormNumForms; @@ -65,29 +66,6 @@ Zotero.Intl = new function () { }; - function getIntlProp(name, fallback = null) { - try { - return intlProps.GetStringFromName(name); - } - catch (e) { - Zotero.logError(`Couldn't load ${name} from intl.properties`); - return fallback; - } - } - - function setOrClearIntlPref(name, type) { - var val = getIntlProp(name); - if (val !== null) { - if (type == 'boolean') { - val = val == 'true'; - } - Zotero.Prefs.set(name, val, true); - } - else { - Zotero.Prefs.clear(name, true); - } - } - /** * @param {String} name * @param {String[]} [params=[]] - Strings to substitute for placeholders @@ -132,4 +110,157 @@ Zotero.Intl = new function () { } return l10n; }; + + /* + * Compares two strings based on the current collator. + * @param {String} string1 + * @param {String} string2 + * @return {Number} a number indicating how string1 and string2 compare to + * each other according to the sort order of this Collator object: a + * negative value if string1 comes before string2; a positive value if + * string1 comes after string2; 0 if they are considered equal. + */ + this.compare = function (...args) { + return this.collation.compareString(1, ...args); + }; + + Object.defineProperty(this, 'collation', { + get() { + if (collation == null) { + collation = getLocaleCollation(); + } + return collation; + } + }); + + + function getIntlProp(name, fallback = null) { + try { + return intlProps.GetStringFromName(name); + } + catch (e) { + Zotero.logError(`Couldn't load ${name} from intl.properties`); + return fallback; + } + } + + function setOrClearIntlPref(name, type) { + var val = getIntlProp(name); + if (val !== null) { + if (type == 'boolean') { + val = val == 'true'; + } + Zotero.Prefs.set(name, val, true); + } + else { + Zotero.Prefs.clear(name, true); + } + } + + function getLocaleCollation() { + try { + // DEBUG: Is this necessary, or will Intl.Collator just default to the same locales we're + // passing manually? + + let locales; + // Fx55+ + if (Services.locale.getAppLocalesAsBCP47) { + locales = Services.locale.getAppLocalesAsBCP47(); + } + else { + let locale; + // Fx54 + if (Services.locale.getAppLocale) { + locale = Services.locale.getAppLocale(); + } + // Fx <=53 + else { + locale = Services.locale.getApplicationLocale(); + locale = locale.getCategory('NSILOCALE_COLLATE'); + } + + // Extract a valid language tag + try { + locale = locale.match(/^[a-z]{2}(\-[A-Z]{2})?/)[0]; + } + catch (e) { + throw new Error(`Error parsing locale ${locale}`); + } + locales = [locale]; + } + + var collator = new Intl.Collator(locales, { + numeric: true, + sensitivity: 'base' + }); + } + catch (e) { + Zotero.logError(e); + + // Fall back to en-US sorting + try { + Zotero.logError("Falling back to en-US sorting"); + collator = new Intl.Collator(['en-US'], { + numeric: true, + sensitivity: 'base' + }); + } + catch (e) { + Zotero.logError(e); + + // If there's still an error, just skip sorting + collator = { + compare: function (a, b) { + return 0; + } + }; + } + } + + // Grab all ASCII punctuation and space at the begining of string + var initPunctuationRE = /^[\x20-\x2F\x3A-\x40\x5B-\x60\x7B-\x7E]+/; + // Punctuation that should be ignored when sorting + var ignoreInitRE = /["'[{(]+$/; + + // Until old code is updated, pretend we're returning an nsICollation + return this.collation = { + compareString: function (_, a, b) { + if (!a && !b) return 0; + if (!a || !b) return b ? -1 : 1; + + // Compare initial punctuation + var aInitP = initPunctuationRE.exec(a) || ''; + var bInitP = initPunctuationRE.exec(b) || ''; + + var aWordStart = 0, bWordStart = 0; + if (aInitP) { + aWordStart = aInitP[0].length; + aInitP = aInitP[0].replace(ignoreInitRE, ''); + } + if (bInitP) { + bWordStart = bInitP.length; + bInitP = bInitP[0].replace(ignoreInitRE, ''); + } + + // If initial punctuation is equivalent, use collator comparison + // that ignores all punctuation + // + // Update: Intl.Collator's ignorePunctuation also ignores whitespace, so we're + // no longer using it, meaning we could take out most of the code to handle + // initial punctuation separately, unless we think we'll at some point switch to + // a collation function that ignores punctuation but not whitespace. + if (aInitP == bInitP || !aInitP && !bInitP) return collator.compare(a, b); + + // Otherwise consider "attached" words as well, e.g. the order should be + // "__ n", "__z", "_a" + // We don't actually care what the attached word is, just whether it's + // there, since at this point we're guaranteed to have non-equivalent + // initial punctuation + if (aWordStart < a.length) aInitP += 'a'; + if (bWordStart < b.length) bInitP += 'a'; + + return aInitP.localeCompare(bInitP); + } + }; + } }; diff --git a/chrome/content/zotero/xpcom/zotero.js b/chrome/content/zotero/xpcom/zotero.js index 585cd659f7..4e10d5784d 100644 --- a/chrome/content/zotero/xpcom/zotero.js +++ b/chrome/content/zotero/xpcom/zotero.js @@ -1412,122 +1412,10 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js"); this.getLocaleCollation = function () { - if (this.collation) { - return this.collation; - } - - try { - // DEBUG: Is this necessary, or will Intl.Collator just default to the same locales we're - // passing manually? - - let locales; - // Fx55+ - if (Services.locale.getAppLocalesAsBCP47) { - locales = Services.locale.getAppLocalesAsBCP47(); - } - else { - let locale; - // Fx54 - if (Services.locale.getAppLocale) { - locale = Services.locale.getAppLocale(); - } - // Fx <=53 - else { - locale = Services.locale.getApplicationLocale(); - locale = locale.getCategory('NSILOCALE_COLLATE'); - } - - // Extract a valid language tag - try { - locale = locale.match(/^[a-z]{2}(\-[A-Z]{2})?/)[0]; - } - catch (e) { - throw new Error(`Error parsing locale ${locale}`); - } - locales = [locale]; - } - - var collator = new Intl.Collator(locales, { - numeric: true, - sensitivity: 'base' - }); - } - catch (e) { - Zotero.logError(e); - - // Fall back to en-US sorting - try { - Zotero.logError("Falling back to en-US sorting"); - collator = new Intl.Collator(['en-US'], { - numeric: true, - sensitivity: 'base' - }); - } - catch (e) { - Zotero.logError(e); - - // If there's still an error, just skip sorting - collator = { - compare: function (a, b) { - return 0; - } - }; - } - } - - // Grab all ASCII punctuation and space at the begining of string - var initPunctuationRE = /^[\x20-\x2F\x3A-\x40\x5B-\x60\x7B-\x7E]+/; - // Punctuation that should be ignored when sorting - var ignoreInitRE = /["'[{(]+$/; - - // Until old code is updated, pretend we're returning an nsICollation - return this.collation = { - compareString: function (_, a, b) { - if (!a && !b) return 0; - if (!a || !b) return b ? -1 : 1; - - // Compare initial punctuation - var aInitP = initPunctuationRE.exec(a) || ''; - var bInitP = initPunctuationRE.exec(b) || ''; - - var aWordStart = 0, bWordStart = 0; - if (aInitP) { - aWordStart = aInitP[0].length; - aInitP = aInitP[0].replace(ignoreInitRE, ''); - } - if (bInitP) { - bWordStart = bInitP.length; - bInitP = bInitP[0].replace(ignoreInitRE, ''); - } - - // If initial punctuation is equivalent, use collator comparison - // that ignores all punctuation - // - // Update: Intl.Collator's ignorePunctuation also ignores whitespace, so we're - // no longer using it, meaning we could take out most of the code to handle - // initial punctuation separately, unless we think we'll at some point switch to - // a collation function that ignores punctuation but not whitespace. - if (aInitP == bInitP || !aInitP && !bInitP) return collator.compare(a, b); - - // Otherwise consider "attached" words as well, e.g. the order should be - // "__ n", "__z", "_a" - // We don't actually care what the attached word is, just whether it's - // there, since at this point we're guaranteed to have non-equivalent - // initial punctuation - if (aWordStart < a.length) aInitP += 'a'; - if (bWordStart < b.length) bInitP += 'a'; - - return aInitP.localeCompare(bInitP); - } - }; - } - - this.defineProperty(this, "localeCompare", { - get: function() { - var collation = this.getLocaleCollation(); - return collation.compareString.bind(collation, 1); - } - }, {lazy: true}); + return Zotero.Intl.collation; + }; + + this.localeCompare = Zotero.Intl.compare; /* * Sets font size based on prefs -- intended for use on root element