michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: this.EXPORTED_SYMBOLS = [ "CharsetMenu" ]; michael@0: michael@0: const { classes: Cc, interfaces: Ci, utils: Cu} = Components; michael@0: michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: XPCOMUtils.defineLazyGetter(this, "gBundle", function() { michael@0: const kUrl = "chrome://global/locale/charsetMenu.properties"; michael@0: return Services.strings.createBundle(kUrl); michael@0: }); michael@0: michael@0: const kAutoDetectors = [ michael@0: ["off", ""], michael@0: ["ja", "ja_parallel_state_machine"], michael@0: ["ru", "ruprob"], michael@0: ["uk", "ukprob"] michael@0: ]; michael@0: michael@0: /** michael@0: * This set contains encodings that are in the Encoding Standard, except: michael@0: * - XSS-dangerous encodings (except ISO-2022-JP which is assumed to be michael@0: * too common not to be included). michael@0: * - x-user-defined, which practically never makes sense as an end-user-chosen michael@0: * override. michael@0: * - Encodings that IE11 doesn't have in its correspoding menu. michael@0: */ michael@0: const kEncodings = new Set([ michael@0: // Globally relevant michael@0: "UTF-8", michael@0: "windows-1252", michael@0: // Arabic michael@0: "windows-1256", michael@0: "ISO-8859-6", michael@0: // Baltic michael@0: "windows-1257", michael@0: "ISO-8859-4", michael@0: // "ISO-8859-13", // Hidden since not in menu in IE11 michael@0: // Central European michael@0: "windows-1250", michael@0: "ISO-8859-2", michael@0: // Chinese, Simplified michael@0: "gbk", michael@0: // Chinese, Traditional michael@0: "Big5", michael@0: // Cyrillic michael@0: "windows-1251", michael@0: "ISO-8859-5", michael@0: "KOI8-R", michael@0: "KOI8-U", michael@0: "IBM866", // Not in menu in Chromium. Maybe drop this? michael@0: // "x-mac-cyrillic", // Not in menu in IE11 or Chromium. michael@0: // Greek michael@0: "windows-1253", michael@0: "ISO-8859-7", michael@0: // Hebrew michael@0: "windows-1255", michael@0: "ISO-8859-8", michael@0: // Japanese michael@0: "Shift_JIS", michael@0: "EUC-JP", michael@0: "ISO-2022-JP", michael@0: // Korean michael@0: "EUC-KR", michael@0: // Thai michael@0: "windows-874", michael@0: // Turkish michael@0: "windows-1254", michael@0: // Vietnamese michael@0: "windows-1258", michael@0: // Hiding rare European encodings that aren't in the menu in IE11 and would michael@0: // make the menu messy by sorting all over the place michael@0: // "ISO-8859-3", michael@0: // "ISO-8859-10", michael@0: // "ISO-8859-14", michael@0: // "ISO-8859-15", michael@0: // "ISO-8859-16", michael@0: // "macintosh" michael@0: ]); michael@0: michael@0: // Always at the start of the menu, in this order, followed by a separator. michael@0: const kPinned = [ michael@0: "UTF-8", michael@0: "windows-1252" michael@0: ]; michael@0: michael@0: kPinned.forEach(x => kEncodings.delete(x)); michael@0: michael@0: function CharsetComparator(a, b) { michael@0: // Normal sorting sorts the part in parenthesis in an order that michael@0: // happens to make the less frequently-used items first. michael@0: let titleA = a.label.replace(/\(.*/, "") + b.value; michael@0: let titleB = b.label.replace(/\(.*/, "") + a.value; michael@0: // Secondarily reverse sort by encoding name to sort "windows" or michael@0: // "shift_jis" first. michael@0: return titleA.localeCompare(titleB) || b.value.localeCompare(a.value); michael@0: } michael@0: michael@0: function SetDetector(event) { michael@0: let str = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString); michael@0: str.data = event.target.getAttribute("detector"); michael@0: Services.prefs.setComplexValue("intl.charset.detector", Ci.nsISupportsString, str); michael@0: } michael@0: michael@0: function UpdateDetectorMenu(event) { michael@0: event.stopPropagation(); michael@0: let detector = Services.prefs.getComplexValue("intl.charset.detector", Ci.nsIPrefLocalizedString); michael@0: let menuitem = this.getElementsByAttribute("detector", detector).item(0); michael@0: if (menuitem) { michael@0: menuitem.setAttribute("checked", "true"); michael@0: } michael@0: } michael@0: michael@0: let gDetectorInfoCache, gCharsetInfoCache, gPinnedInfoCache; michael@0: michael@0: let CharsetMenu = { michael@0: build: function(parent, showAccessKeys=true, showDetector=true) { michael@0: function createDOMNode(doc, nodeInfo) { michael@0: let node = doc.createElement("menuitem"); michael@0: node.setAttribute("type", "radio"); michael@0: node.setAttribute("name", nodeInfo.name + "Group"); michael@0: node.setAttribute(nodeInfo.name, nodeInfo.value); michael@0: node.setAttribute("label", nodeInfo.label); michael@0: if (showAccessKeys && nodeInfo.accesskey) { michael@0: node.setAttribute("accesskey", nodeInfo.accesskey); michael@0: } michael@0: return node; michael@0: } michael@0: michael@0: if (parent.hasChildNodes()) { michael@0: // Detector menu or charset menu already built michael@0: return; michael@0: } michael@0: this._ensureDataReady(); michael@0: let doc = parent.ownerDocument; michael@0: michael@0: if (showDetector) { michael@0: let menuNode = doc.createElement("menu"); michael@0: menuNode.setAttribute("label", gBundle.GetStringFromName("charsetMenuAutodet")); michael@0: if (showAccessKeys) { michael@0: menuNode.setAttribute("accesskey", gBundle.GetStringFromName("charsetMenuAutodet.key")); michael@0: } michael@0: parent.appendChild(menuNode); michael@0: michael@0: let menuPopupNode = doc.createElement("menupopup"); michael@0: menuNode.appendChild(menuPopupNode); michael@0: menuPopupNode.addEventListener("command", SetDetector); michael@0: menuPopupNode.addEventListener("popupshown", UpdateDetectorMenu); michael@0: michael@0: gDetectorInfoCache.forEach(detectorInfo => menuPopupNode.appendChild(createDOMNode(doc, detectorInfo))); michael@0: parent.appendChild(doc.createElement("menuseparator")); michael@0: } michael@0: michael@0: gPinnedInfoCache.forEach(charsetInfo => parent.appendChild(createDOMNode(doc, charsetInfo))); michael@0: parent.appendChild(doc.createElement("menuseparator")); michael@0: gCharsetInfoCache.forEach(charsetInfo => parent.appendChild(createDOMNode(doc, charsetInfo))); michael@0: }, michael@0: michael@0: getData: function() { michael@0: this._ensureDataReady(); michael@0: return { michael@0: detectors: gDetectorInfoCache, michael@0: pinnedCharsets: gPinnedInfoCache, michael@0: otherCharsets: gCharsetInfoCache michael@0: }; michael@0: }, michael@0: michael@0: _ensureDataReady: function() { michael@0: if (!gDetectorInfoCache) { michael@0: gDetectorInfoCache = this.getDetectorInfo(); michael@0: gPinnedInfoCache = this.getCharsetInfo(kPinned, false); michael@0: gCharsetInfoCache = this.getCharsetInfo(kEncodings); michael@0: } michael@0: }, michael@0: michael@0: getDetectorInfo: function() { michael@0: return kAutoDetectors.map(([detectorName, nodeId]) => ({ michael@0: label: this._getDetectorLabel(detectorName), michael@0: accesskey: this._getDetectorAccesskey(detectorName), michael@0: name: "detector", michael@0: value: nodeId michael@0: })); michael@0: }, michael@0: michael@0: getCharsetInfo: function(charsets, sort=true) { michael@0: let list = [{ michael@0: label: this._getCharsetLabel(charset), michael@0: accesskey: this._getCharsetAccessKey(charset), michael@0: name: "charset", michael@0: value: charset michael@0: } for (charset of charsets)]; michael@0: michael@0: if (sort) { michael@0: list.sort(CharsetComparator); michael@0: } michael@0: return list; michael@0: }, michael@0: michael@0: _getDetectorLabel: function(detector) { michael@0: try { michael@0: return gBundle.GetStringFromName("charsetMenuAutodet." + detector); michael@0: } catch (ex) {} michael@0: return detector; michael@0: }, michael@0: _getDetectorAccesskey: function(detector) { michael@0: try { michael@0: return gBundle.GetStringFromName("charsetMenuAutodet." + detector + ".key"); michael@0: } catch (ex) {} michael@0: return ""; michael@0: }, michael@0: michael@0: _getCharsetLabel: function(charset) { michael@0: if (charset == "gbk") { michael@0: // Localization key has been revised michael@0: charset = "gbk.bis"; michael@0: } michael@0: try { michael@0: return gBundle.GetStringFromName(charset); michael@0: } catch (ex) {} michael@0: return charset; michael@0: }, michael@0: _getCharsetAccessKey: function(charset) { michael@0: if (charset == "gbk") { michael@0: // Localization key has been revised michael@0: charset = "gbk.bis"; michael@0: } michael@0: try { michael@0: return gBundle.GetStringFromName(charset + ".key"); michael@0: } catch (ex) {} michael@0: return ""; michael@0: }, michael@0: michael@0: /** michael@0: * For substantially similar encodings, treat two encodings as the same michael@0: * for the purpose of the check mark. michael@0: */ michael@0: foldCharset: function(charset) { michael@0: switch (charset) { michael@0: case "ISO-8859-8-I": michael@0: return "windows-1255"; michael@0: michael@0: case "gb18030": michael@0: return "gbk"; michael@0: michael@0: default: michael@0: return charset; michael@0: } michael@0: }, michael@0: michael@0: update: function(parent, charset) { michael@0: let menuitem = parent.getElementsByAttribute("charset", this.foldCharset(charset)).item(0); michael@0: if (menuitem) { michael@0: menuitem.setAttribute("checked", "true"); michael@0: } michael@0: }, michael@0: }; michael@0: michael@0: Object.freeze(CharsetMenu); michael@0: