toolkit/devtools/server/actors/styleeditor.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this
     3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 "use strict";
     7 let { components, Cc, Ci, Cu } = require("chrome");
     8 let Services = require("Services");
    10 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    11 Cu.import("resource://gre/modules/NetUtil.jsm");
    12 Cu.import("resource://gre/modules/FileUtils.jsm");
    13 Cu.import("resource://gre/modules/devtools/SourceMap.jsm");
    15 const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
    16 const events = require("sdk/event/core");
    17 const protocol = require("devtools/server/protocol");
    18 const {Arg, Option, method, RetVal, types} = protocol;
    19 const {LongStringActor, ShortLongString} = require("devtools/server/actors/string");
    21 loader.lazyGetter(this, "CssLogic", () => require("devtools/styleinspector/css-logic").CssLogic);
    23 let TRANSITION_CLASS = "moz-styleeditor-transitioning";
    24 let TRANSITION_DURATION_MS = 500;
    25 let TRANSITION_RULE = "\
    26 :root.moz-styleeditor-transitioning, :root.moz-styleeditor-transitioning * {\
    27 transition-duration: " + TRANSITION_DURATION_MS + "ms !important; \
    28 transition-delay: 0ms !important;\
    29 transition-timing-function: ease-out !important;\
    30 transition-property: all !important;\
    31 }";
    33 let LOAD_ERROR = "error-load";
    35 exports.register = function(handle) {
    36   handle.addTabActor(StyleEditorActor, "styleEditorActor");
    37   handle.addGlobalActor(StyleEditorActor, "styleEditorActor");
    38 };
    40 exports.unregister = function(handle) {
    41   handle.removeTabActor(StyleEditorActor);
    42   handle.removeGlobalActor(StyleEditorActor);
    43 };
    45 types.addActorType("old-stylesheet");
    47 /**
    48  * Creates a StyleEditorActor. StyleEditorActor provides remote access to the
    49  * stylesheets of a document.
    50  */
    51 let StyleEditorActor = protocol.ActorClass({
    52   typeName: "styleeditor",
    54   /**
    55    * The window we work with, taken from the parent actor.
    56    */
    57   get window() this.parentActor.window,
    59   /**
    60    * The current content document of the window we work with.
    61    */
    62   get document() this.window.document,
    64   events: {
    65     "document-load" : {
    66       type: "documentLoad",
    67       styleSheets: Arg(0, "array:old-stylesheet")
    68     }
    69   },
    71   form: function()
    72   {
    73     return { actor: this.actorID };
    74   },
    76   initialize: function (conn, tabActor) {
    77     protocol.Actor.prototype.initialize.call(this, null);
    79     this.parentActor = tabActor;
    81     // keep a map of sheets-to-actors so we don't create two actors for one sheet
    82     this._sheets = new Map();
    83   },
    85   /**
    86    * Destroy the current StyleEditorActor instance.
    87    */
    88   destroy: function()
    89   {
    90     this._sheets.clear();
    91   },
    93   /**
    94    * Called by client when target navigates to a new document.
    95    * Adds load listeners to document.
    96    */
    97   newDocument: method(function() {
    98     // delete previous document's actors
    99     this._clearStyleSheetActors();
   101     // Note: listening for load won't be necessary once
   102     // https://bugzilla.mozilla.org/show_bug.cgi?id=839103 is fixed
   103     if (this.document.readyState == "complete") {
   104       this._onDocumentLoaded();
   105     }
   106     else {
   107       this.window.addEventListener("load", this._onDocumentLoaded, false);
   108     }
   109     return {};
   110   }),
   112   /**
   113    * Event handler for document loaded event. Add actor for each stylesheet
   114    * and send an event notifying of the load
   115    */
   116   _onDocumentLoaded: function(event) {
   117     if (event) {
   118       this.window.removeEventListener("load", this._onDocumentLoaded, false);
   119     }
   121     let documents = [this.document];
   122     var forms = [];
   123     for (let doc of documents) {
   124       let sheetForms = this._addStyleSheets(doc.styleSheets);
   125       forms = forms.concat(sheetForms);
   126       // Recursively handle style sheets of the documents in iframes.
   127       for (let iframe of doc.getElementsByTagName("iframe")) {
   128         documents.push(iframe.contentDocument);
   129       }
   130     }
   132     events.emit(this, "document-load", forms);
   133   },
   135   /**
   136    * Add all the stylesheets to the map and create an actor for each one
   137    * if not already created. Send event that there are new stylesheets.
   138    *
   139    * @param {[DOMStyleSheet]} styleSheets
   140    *        Stylesheets to add
   141    * @return {[object]}
   142    *         Array of actors for each StyleSheetActor created
   143    */
   144   _addStyleSheets: function(styleSheets)
   145   {
   146     let sheets = [];
   147     for (let i = 0; i < styleSheets.length; i++) {
   148       let styleSheet = styleSheets[i];
   149       sheets.push(styleSheet);
   151       // Get all sheets, including imported ones
   152       let imports = this._getImported(styleSheet);
   153       sheets = sheets.concat(imports);
   154     }
   155     let actors = sheets.map(this._createStyleSheetActor.bind(this));
   157     return actors;
   158   },
   160   /**
   161    * Create a new actor for a style sheet, if it hasn't already been created.
   162    *
   163    * @param  {DOMStyleSheet} styleSheet
   164    *         The style sheet to create an actor for.
   165    * @return {StyleSheetActor}
   166    *         The actor for this style sheet
   167    */
   168   _createStyleSheetActor: function(styleSheet)
   169   {
   170     if (this._sheets.has(styleSheet)) {
   171       return this._sheets.get(styleSheet);
   172     }
   173     let actor = new OldStyleSheetActor(styleSheet, this);
   175     this.manage(actor);
   176     this._sheets.set(styleSheet, actor);
   178     return actor;
   179   },
   181   /**
   182    * Get all the stylesheets @imported from a stylesheet.
   183    *
   184    * @param  {DOMStyleSheet} styleSheet
   185    *         Style sheet to search
   186    * @return {array}
   187    *         All the imported stylesheets
   188    */
   189   _getImported: function(styleSheet) {
   190    let imported = [];
   192    for (let i = 0; i < styleSheet.cssRules.length; i++) {
   193       let rule = styleSheet.cssRules[i];
   194       if (rule.type == Ci.nsIDOMCSSRule.IMPORT_RULE) {
   195         // Associated styleSheet may be null if it has already been seen due to
   196         // duplicate @imports for the same URL.
   197         if (!rule.styleSheet) {
   198           continue;
   199         }
   200         imported.push(rule.styleSheet);
   202         // recurse imports in this stylesheet as well
   203         imported = imported.concat(this._getImported(rule.styleSheet));
   204       }
   205       else if (rule.type != Ci.nsIDOMCSSRule.CHARSET_RULE) {
   206         // @import rules must precede all others except @charset
   207         break;
   208       }
   209     }
   210     return imported;
   211   },
   213   /**
   214    * Clear all the current stylesheet actors in map.
   215    */
   216   _clearStyleSheetActors: function() {
   217     for (let actor in this._sheets) {
   218       this.unmanage(this._sheets[actor]);
   219     }
   220     this._sheets.clear();
   221   },
   223   /**
   224    * Create a new style sheet in the document with the given text.
   225    * Return an actor for it.
   226    *
   227    * @param  {object} request
   228    *         Debugging protocol request object, with 'text property'
   229    * @return {object}
   230    *         Object with 'styelSheet' property for form on new actor.
   231    */
   232   newStyleSheet: method(function(text) {
   233     let parent = this.document.documentElement;
   234     let style = this.document.createElementNS("http://www.w3.org/1999/xhtml", "style");
   235     style.setAttribute("type", "text/css");
   237     if (text) {
   238       style.appendChild(this.document.createTextNode(text));
   239     }
   240     parent.appendChild(style);
   242     let actor = this._createStyleSheetActor(style.sheet);
   243     return actor;
   244   }, {
   245     request: { text: Arg(0, "string") },
   246     response: { styleSheet: RetVal("old-stylesheet") }
   247   })
   248 });
   250 /**
   251  * The corresponding Front object for the StyleEditorActor.
   252  */
   253 let StyleEditorFront = protocol.FrontClass(StyleEditorActor, {
   254   initialize: function(client, tabForm) {
   255     protocol.Front.prototype.initialize.call(this, client);
   256     this.actorID = tabForm.styleEditorActor;
   258     client.addActorPool(this);
   259     this.manage(this);
   260   },
   262   getStyleSheets: function() {
   263     let deferred = promise.defer();
   265     events.once(this, "document-load", (styleSheets) => {
   266       deferred.resolve(styleSheets);
   267     });
   268     this.newDocument();
   270     return deferred.promise;
   271   },
   273   addStyleSheet: function(text) {
   274     return this.newStyleSheet(text);
   275   }
   276 });
   278 /**
   279  * A StyleSheetActor represents a stylesheet on the server.
   280  */
   281 let OldStyleSheetActor = protocol.ActorClass({
   282   typeName: "old-stylesheet",
   284   events: {
   285     "property-change" : {
   286       type: "propertyChange",
   287       property: Arg(0, "string"),
   288       value: Arg(1, "json")
   289     },
   290     "source-load" : {
   291       type: "sourceLoad",
   292       source: Arg(0, "string")
   293     },
   294     "style-applied" : {
   295       type: "styleApplied"
   296     }
   297   },
   299   toString: function() {
   300     return "[OldStyleSheetActor " + this.actorID + "]";
   301   },
   303   /**
   304    * Window of target
   305    */
   306   get window() this._window || this.parentActor.window,
   308   /**
   309    * Document of target.
   310    */
   311   get document() this.window.document,
   313   /**
   314    * URL of underlying stylesheet.
   315    */
   316   get href() this.rawSheet.href,
   318   /**
   319    * Retrieve the index (order) of stylesheet in the document.
   320    *
   321    * @return number
   322    */
   323   get styleSheetIndex()
   324   {
   325     if (this._styleSheetIndex == -1) {
   326       for (let i = 0; i < this.document.styleSheets.length; i++) {
   327         if (this.document.styleSheets[i] == this.rawSheet) {
   328           this._styleSheetIndex = i;
   329           break;
   330         }
   331       }
   332     }
   333     return this._styleSheetIndex;
   334   },
   336   initialize: function(aStyleSheet, aParentActor, aWindow) {
   337     protocol.Actor.prototype.initialize.call(this, null);
   339     this.rawSheet = aStyleSheet;
   340     this.parentActor = aParentActor;
   341     this.conn = this.parentActor.conn;
   343     this._window = aWindow;
   345     // text and index are unknown until source load
   346     this.text = null;
   347     this._styleSheetIndex = -1;
   349     this._transitionRefCount = 0;
   351     // if this sheet has an @import, then it's rules are loaded async
   352     let ownerNode = this.rawSheet.ownerNode;
   353     if (ownerNode) {
   354       let onSheetLoaded = function(event) {
   355         ownerNode.removeEventListener("load", onSheetLoaded, false);
   356         this._notifyPropertyChanged("ruleCount");
   357       }.bind(this);
   359       ownerNode.addEventListener("load", onSheetLoaded, false);
   360     }
   361   },
   363   /**
   364    * Get the current state of the actor
   365    *
   366    * @return {object}
   367    *         With properties of the underlying stylesheet, plus 'text',
   368    *        'styleSheetIndex' and 'parentActor' if it's @imported
   369    */
   370   form: function(detail) {
   371     if (detail === "actorid") {
   372       return this.actorID;
   373     }
   375     let docHref;
   376     if (this.rawSheet.ownerNode) {
   377       if (this.rawSheet.ownerNode instanceof Ci.nsIDOMHTMLDocument) {
   378         docHref = this.rawSheet.ownerNode.location.href;
   379       }
   380       if (this.rawSheet.ownerNode.ownerDocument) {
   381         docHref = this.rawSheet.ownerNode.ownerDocument.location.href;
   382       }
   383     }
   385     let form = {
   386       actor: this.actorID,  // actorID is set when this actor is added to a pool
   387       href: this.href,
   388       nodeHref: docHref,
   389       disabled: this.rawSheet.disabled,
   390       title: this.rawSheet.title,
   391       system: !CssLogic.isContentStylesheet(this.rawSheet),
   392       styleSheetIndex: this.styleSheetIndex
   393     }
   395     try {
   396       form.ruleCount = this.rawSheet.cssRules.length;
   397     }
   398     catch(e) {
   399       // stylesheet had an @import rule that wasn't loaded yet
   400     }
   401     return form;
   402   },
   404   /**
   405    * Toggle the disabled property of the style sheet
   406    *
   407    * @return {object}
   408    *         'disabled' - the disabled state after toggling.
   409    */
   410   toggleDisabled: method(function() {
   411     this.rawSheet.disabled = !this.rawSheet.disabled;
   412     this._notifyPropertyChanged("disabled");
   414     return this.rawSheet.disabled;
   415   }, {
   416     response: { disabled: RetVal("boolean")}
   417   }),
   419   /**
   420    * Send an event notifying that a property of the stylesheet
   421    * has changed.
   422    *
   423    * @param  {string} property
   424    *         Name of the changed property
   425    */
   426   _notifyPropertyChanged: function(property) {
   427     events.emit(this, "property-change", property, this.form()[property]);
   428   },
   430    /**
   431     * Fetch the source of the style sheet from its URL. Send a "sourceLoad"
   432     * event when it's been fetched.
   433     */
   434   fetchSource: method(function() {
   435     this._getText().then((content) => {
   436       events.emit(this, "source-load", this.text);
   437     });
   438   }),
   440   /**
   441    * Fetch the text for this stylesheet from the cache or network. Return
   442    * cached text if it's already been fetched.
   443    *
   444    * @return {Promise}
   445    *         Promise that resolves with a string text of the stylesheet.
   446    */
   447   _getText: function() {
   448     if (this.text) {
   449       return promise.resolve(this.text);
   450     }
   452     if (!this.href) {
   453       // this is an inline <style> sheet
   454       let content = this.rawSheet.ownerNode.textContent;
   455       this.text = content;
   456       return promise.resolve(content);
   457     }
   459     let options = {
   460       window: this.window,
   461       charset: this._getCSSCharset()
   462     };
   464     return fetch(this.href, options).then(({ content }) => {
   465       this.text = content;
   466       return content;
   467     });
   468   },
   470   /**
   471    * Get the charset of the stylesheet according to the character set rules
   472    * defined in <http://www.w3.org/TR/CSS2/syndata.html#charset>.
   473    *
   474    * @param string channelCharset
   475    *        Charset of the source string if set by the HTTP channel.
   476    */
   477   _getCSSCharset: function(channelCharset)
   478   {
   479     // StyleSheet's charset can be specified from multiple sources
   480     if (channelCharset && channelCharset.length > 0) {
   481       // step 1 of syndata.html: charset given in HTTP header.
   482       return channelCharset;
   483     }
   485     let sheet = this.rawSheet;
   486     if (sheet) {
   487       // Do we have a @charset rule in the stylesheet?
   488       // step 2 of syndata.html (without the BOM check).
   489       if (sheet.cssRules) {
   490         let rules = sheet.cssRules;
   491         if (rules.length
   492             && rules.item(0).type == Ci.nsIDOMCSSRule.CHARSET_RULE) {
   493           return rules.item(0).encoding;
   494         }
   495       }
   497       // step 3: charset attribute of <link> or <style> element, if it exists
   498       if (sheet.ownerNode && sheet.ownerNode.getAttribute) {
   499         let linkCharset = sheet.ownerNode.getAttribute("charset");
   500         if (linkCharset != null) {
   501           return linkCharset;
   502         }
   503       }
   505       // step 4 (1 of 2): charset of referring stylesheet.
   506       let parentSheet = sheet.parentStyleSheet;
   507       if (parentSheet && parentSheet.cssRules &&
   508           parentSheet.cssRules[0].type == Ci.nsIDOMCSSRule.CHARSET_RULE) {
   509         return parentSheet.cssRules[0].encoding;
   510       }
   512       // step 4 (2 of 2): charset of referring document.
   513       if (sheet.ownerNode && sheet.ownerNode.ownerDocument.characterSet) {
   514         return sheet.ownerNode.ownerDocument.characterSet;
   515       }
   516     }
   518     // step 5: default to utf-8.
   519     return "UTF-8";
   520   },
   522   /**
   523    * Update the style sheet in place with new text.
   524    *
   525    * @param  {object} request
   526    *         'text' - new text
   527    *         'transition' - whether to do CSS transition for change.
   528    */
   529   update: method(function(text, transition) {
   530     DOMUtils.parseStyleSheet(this.rawSheet, text);
   532     this.text = text;
   534     this._notifyPropertyChanged("ruleCount");
   536     if (transition) {
   537       this._insertTransistionRule();
   538     }
   539     else {
   540       this._notifyStyleApplied();
   541     }
   542   }, {
   543     request: {
   544       text: Arg(0, "string"),
   545       transition: Arg(1, "boolean")
   546     }
   547   }),
   549   /**
   550    * Insert a catch-all transition rule into the document. Set a timeout
   551    * to remove the rule after a certain time.
   552    */
   553   _insertTransistionRule: function() {
   554     // Insert the global transition rule
   555     // Use a ref count to make sure we do not add it multiple times.. and remove
   556     // it only when all pending StyleEditor-generated transitions ended.
   557     if (this._transitionRefCount == 0) {
   558       this.rawSheet.insertRule(TRANSITION_RULE, this.rawSheet.cssRules.length);
   559       this.document.documentElement.classList.add(TRANSITION_CLASS);
   560     }
   562     this._transitionRefCount++;
   564     // Set up clean up and commit after transition duration (+10% buffer)
   565     // @see _onTransitionEnd
   566     this.window.setTimeout(this._onTransitionEnd.bind(this),
   567                            Math.floor(TRANSITION_DURATION_MS * 1.1));
   568   },
   570   /**
   571     * This cleans up class and rule added for transition effect and then
   572     * notifies that the style has been applied.
   573     */
   574   _onTransitionEnd: function()
   575   {
   576     if (--this._transitionRefCount == 0) {
   577       this.document.documentElement.classList.remove(TRANSITION_CLASS);
   578       this.rawSheet.deleteRule(this.rawSheet.cssRules.length - 1);
   579     }
   581     events.emit(this, "style-applied");
   582   }
   583 })
   585 /**
   586  * StyleSheetFront is the client-side counterpart to a StyleSheetActor.
   587  */
   588 var OldStyleSheetFront = protocol.FrontClass(OldStyleSheetActor, {
   589   initialize: function(conn, form, ctx, detail) {
   590     protocol.Front.prototype.initialize.call(this, conn, form, ctx, detail);
   592     this._onPropertyChange = this._onPropertyChange.bind(this);
   593     events.on(this, "property-change", this._onPropertyChange);
   594   },
   596   destroy: function() {
   597     events.off(this, "property-change", this._onPropertyChange);
   599     protocol.Front.prototype.destroy.call(this);
   600   },
   602   _onPropertyChange: function(property, value) {
   603     this._form[property] = value;
   604   },
   606   form: function(form, detail) {
   607     if (detail === "actorid") {
   608       this.actorID = form;
   609       return;
   610     }
   611     this.actorID = form.actor;
   612     this._form = form;
   613   },
   615   getText: function() {
   616     let deferred = promise.defer();
   618     events.once(this, "source-load", (source) => {
   619       let longStr = new ShortLongString(source);
   620       deferred.resolve(longStr);
   621     });
   622     this.fetchSource();
   624     return deferred.promise;
   625   },
   627   getOriginalSources: function() {
   628     return promise.resolve([]);
   629   },
   631   get href() this._form.href,
   632   get nodeHref() this._form.nodeHref,
   633   get disabled() !!this._form.disabled,
   634   get title() this._form.title,
   635   get isSystem() this._form.system,
   636   get styleSheetIndex() this._form.styleSheetIndex,
   637   get ruleCount() this._form.ruleCount
   638 });
   640 XPCOMUtils.defineLazyGetter(this, "DOMUtils", function () {
   641   return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
   642 });
   644 exports.StyleEditorActor = StyleEditorActor;
   645 exports.StyleEditorFront = StyleEditorFront;
   647 exports.OldStyleSheetActor = OldStyleSheetActor;
   648 exports.OldStyleSheetFront = OldStyleSheetFront;
   651 /**
   652  * Performs a request to load the desired URL and returns a promise.
   653  *
   654  * @param aURL String
   655  *        The URL we will request.
   656  * @returns Promise
   657  *        A promise of the document at that URL, as a string.
   658  */
   659 function fetch(aURL, aOptions={ loadFromCache: true, window: null,
   660                                 charset: null}) {
   661   let deferred = promise.defer();
   662   let scheme;
   663   let url = aURL.split(" -> ").pop();
   664   let charset;
   665   let contentType;
   667   try {
   668     scheme = Services.io.extractScheme(url);
   669   } catch (e) {
   670     // In the xpcshell tests, the script url is the absolute path of the test
   671     // file, which will make a malformed URI error be thrown. Add the file
   672     // scheme prefix ourselves.
   673     url = "file://" + url;
   674     scheme = Services.io.extractScheme(url);
   675   }
   677   switch (scheme) {
   678     case "file":
   679     case "chrome":
   680     case "resource":
   681       try {
   682         NetUtil.asyncFetch(url, function onFetch(aStream, aStatus, aRequest) {
   683           if (!components.isSuccessCode(aStatus)) {
   684             deferred.reject(new Error("Request failed with status code = "
   685                                       + aStatus
   686                                       + " after NetUtil.asyncFetch for url = "
   687                                       + url));
   688             return;
   689           }
   691           let source = NetUtil.readInputStreamToString(aStream, aStream.available());
   692           contentType = aRequest.contentType;
   693           deferred.resolve(source);
   694           aStream.close();
   695         });
   696       } catch (ex) {
   697         deferred.reject(ex);
   698       }
   699       break;
   701     default:
   702       let channel;
   703       try {
   704         channel = Services.io.newChannel(url, null, null);
   705       } catch (e if e.name == "NS_ERROR_UNKNOWN_PROTOCOL") {
   706         // On Windows xpcshell tests, c:/foo/bar can pass as a valid URL, but
   707         // newChannel won't be able to handle it.
   708         url = "file:///" + url;
   709         channel = Services.io.newChannel(url, null, null);
   710       }
   711       let chunks = [];
   712       let streamListener = {
   713         onStartRequest: function(aRequest, aContext, aStatusCode) {
   714           if (!components.isSuccessCode(aStatusCode)) {
   715             deferred.reject(new Error("Request failed with status code = "
   716                                       + aStatusCode
   717                                       + " in onStartRequest handler for url = "
   718                                       + url));
   719           }
   720         },
   721         onDataAvailable: function(aRequest, aContext, aStream, aOffset, aCount) {
   722           chunks.push(NetUtil.readInputStreamToString(aStream, aCount));
   723         },
   724         onStopRequest: function(aRequest, aContext, aStatusCode) {
   725           if (!components.isSuccessCode(aStatusCode)) {
   726             deferred.reject(new Error("Request failed with status code = "
   727                                       + aStatusCode
   728                                       + " in onStopRequest handler for url = "
   729                                       + url));
   730             return;
   731           }
   733           charset = channel.contentCharset || charset;
   734           contentType = channel.contentType;
   735           deferred.resolve(chunks.join(""));
   736         }
   737       };
   739       if (aOptions.window) {
   740         // respect private browsing
   741         channel.loadGroup = aOptions.window.QueryInterface(Ci.nsIInterfaceRequestor)
   742                               .getInterface(Ci.nsIWebNavigation)
   743                               .QueryInterface(Ci.nsIDocumentLoader)
   744                               .loadGroup;
   745       }
   746       channel.loadFlags = aOptions.loadFromCache
   747         ? channel.LOAD_FROM_CACHE
   748         : channel.LOAD_BYPASS_CACHE;
   749       channel.asyncOpen(streamListener, null);
   750       break;
   751   }
   753   return deferred.promise.then(source => {
   754     return {
   755       content: convertToUnicode(source, charset),
   756       contentType: contentType
   757     };
   758   });
   759 }
   761 /**
   762  * Convert a given string, encoded in a given character set, to unicode.
   763  *
   764  * @param string aString
   765  *        A string.
   766  * @param string aCharset
   767  *        A character set.
   768  */
   769 function convertToUnicode(aString, aCharset=null) {
   770   // Decoding primitives.
   771   let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
   772     .createInstance(Ci.nsIScriptableUnicodeConverter);
   773   try {
   774     converter.charset = aCharset || "UTF-8";
   775     return converter.ConvertToUnicode(aString);
   776   } catch(e) {
   777     return aString;
   778   }
   779 }
   781 /**
   782  * Normalize multiple relative paths towards the base paths on the right.
   783  */
   784 function normalize(...aURLs) {
   785   let base = Services.io.newURI(aURLs.pop(), null, null);
   786   let url;
   787   while ((url = aURLs.pop())) {
   788     base = Services.io.newURI(url, null, base);
   789   }
   790   return base.spec;
   791 }
   793 function dirname(aPath) {
   794   return Services.io.newURI(
   795     ".", null, Services.io.newURI(aPath, null, null)).spec;
   796 }

mercurial