toolkit/devtools/server/actors/stylesheets.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.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 "use strict";
michael@0 6
michael@0 7 let { components, Cc, Ci, Cu } = require("chrome");
michael@0 8 let Services = require("Services");
michael@0 9
michael@0 10 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 11 Cu.import("resource://gre/modules/NetUtil.jsm");
michael@0 12 Cu.import("resource://gre/modules/FileUtils.jsm");
michael@0 13 Cu.import("resource://gre/modules/devtools/SourceMap.jsm");
michael@0 14 Cu.import("resource://gre/modules/Task.jsm");
michael@0 15
michael@0 16 const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
michael@0 17 const events = require("sdk/event/core");
michael@0 18 const protocol = require("devtools/server/protocol");
michael@0 19 const {Arg, Option, method, RetVal, types} = protocol;
michael@0 20 const {LongStringActor, ShortLongString} = require("devtools/server/actors/string");
michael@0 21
michael@0 22 loader.lazyGetter(this, "CssLogic", () => require("devtools/styleinspector/css-logic").CssLogic);
michael@0 23
michael@0 24 let TRANSITION_CLASS = "moz-styleeditor-transitioning";
michael@0 25 let TRANSITION_DURATION_MS = 500;
michael@0 26 let TRANSITION_BUFFER_MS = 1000;
michael@0 27 let TRANSITION_RULE = "\
michael@0 28 :root.moz-styleeditor-transitioning, :root.moz-styleeditor-transitioning * {\
michael@0 29 transition-duration: " + TRANSITION_DURATION_MS + "ms !important; \
michael@0 30 transition-delay: 0ms !important;\
michael@0 31 transition-timing-function: ease-out !important;\
michael@0 32 transition-property: all !important;\
michael@0 33 }";
michael@0 34
michael@0 35 let LOAD_ERROR = "error-load";
michael@0 36
michael@0 37 exports.register = function(handle) {
michael@0 38 handle.addTabActor(StyleSheetsActor, "styleSheetsActor");
michael@0 39 handle.addGlobalActor(StyleSheetsActor, "styleSheetsActor");
michael@0 40 };
michael@0 41
michael@0 42 exports.unregister = function(handle) {
michael@0 43 handle.removeTabActor(StyleSheetsActor);
michael@0 44 handle.removeGlobalActor(StyleSheetsActor);
michael@0 45 };
michael@0 46
michael@0 47 types.addActorType("stylesheet");
michael@0 48 types.addActorType("originalsource");
michael@0 49
michael@0 50 /**
michael@0 51 * Creates a StyleSheetsActor. StyleSheetsActor provides remote access to the
michael@0 52 * stylesheets of a document.
michael@0 53 */
michael@0 54 let StyleSheetsActor = protocol.ActorClass({
michael@0 55 typeName: "stylesheets",
michael@0 56
michael@0 57 /**
michael@0 58 * The window we work with, taken from the parent actor.
michael@0 59 */
michael@0 60 get window() this.parentActor.window,
michael@0 61
michael@0 62 /**
michael@0 63 * The current content document of the window we work with.
michael@0 64 */
michael@0 65 get document() this.window.document,
michael@0 66
michael@0 67 form: function()
michael@0 68 {
michael@0 69 return { actor: this.actorID };
michael@0 70 },
michael@0 71
michael@0 72 initialize: function (conn, tabActor) {
michael@0 73 protocol.Actor.prototype.initialize.call(this, null);
michael@0 74
michael@0 75 this.parentActor = tabActor;
michael@0 76
michael@0 77 // keep a map of sheets-to-actors so we don't create two actors for one sheet
michael@0 78 this._sheets = new Map();
michael@0 79 },
michael@0 80
michael@0 81 /**
michael@0 82 * Destroy the current StyleSheetsActor instance.
michael@0 83 */
michael@0 84 destroy: function()
michael@0 85 {
michael@0 86 this._sheets.clear();
michael@0 87 },
michael@0 88
michael@0 89 /**
michael@0 90 * Protocol method for getting a list of StyleSheetActors representing
michael@0 91 * all the style sheets in this document.
michael@0 92 */
michael@0 93 getStyleSheets: method(function() {
michael@0 94 let deferred = promise.defer();
michael@0 95
michael@0 96 let window = this.window;
michael@0 97 var domReady = () => {
michael@0 98 window.removeEventListener("DOMContentLoaded", domReady, true);
michael@0 99 this._addAllStyleSheets().then(deferred.resolve, Cu.reportError);
michael@0 100 };
michael@0 101
michael@0 102 if (window.document.readyState === "loading") {
michael@0 103 window.addEventListener("DOMContentLoaded", domReady, true);
michael@0 104 } else {
michael@0 105 domReady();
michael@0 106 }
michael@0 107
michael@0 108 return deferred.promise;
michael@0 109 }, {
michael@0 110 request: {},
michael@0 111 response: { styleSheets: RetVal("array:stylesheet") }
michael@0 112 }),
michael@0 113
michael@0 114 /**
michael@0 115 * Add all the stylesheets in this document and its subframes.
michael@0 116 * Assumes the document is loaded.
michael@0 117 *
michael@0 118 * @return {Promise}
michael@0 119 * Promise that resolves with an array of StyleSheetActors
michael@0 120 */
michael@0 121 _addAllStyleSheets: function() {
michael@0 122 return Task.spawn(function() {
michael@0 123 let documents = [this.document];
michael@0 124 let actors = [];
michael@0 125
michael@0 126 for (let doc of documents) {
michael@0 127 let sheets = yield this._addStyleSheets(doc.styleSheets);
michael@0 128 actors = actors.concat(sheets);
michael@0 129
michael@0 130 // Recursively handle style sheets of the documents in iframes.
michael@0 131 for (let iframe of doc.getElementsByTagName("iframe")) {
michael@0 132 if (iframe.contentDocument) {
michael@0 133 // Sometimes, iframes don't have any document, like the
michael@0 134 // one that are over deeply nested (bug 285395)
michael@0 135 documents.push(iframe.contentDocument);
michael@0 136 }
michael@0 137 }
michael@0 138 }
michael@0 139 throw new Task.Result(actors);
michael@0 140 }.bind(this));
michael@0 141 },
michael@0 142
michael@0 143 /**
michael@0 144 * Add all the stylesheets to the map and create an actor for each one
michael@0 145 * if not already created.
michael@0 146 *
michael@0 147 * @param {[DOMStyleSheet]} styleSheets
michael@0 148 * Stylesheets to add
michael@0 149 *
michael@0 150 * @return {Promise}
michael@0 151 * Promise that resolves to an array of StyleSheetActors
michael@0 152 */
michael@0 153 _addStyleSheets: function(styleSheets)
michael@0 154 {
michael@0 155 return Task.spawn(function() {
michael@0 156 let actors = [];
michael@0 157 for (let i = 0; i < styleSheets.length; i++) {
michael@0 158 let actor = this._createStyleSheetActor(styleSheets[i]);
michael@0 159 actors.push(actor);
michael@0 160
michael@0 161 // Get all sheets, including imported ones
michael@0 162 let imports = yield this._getImported(actor);
michael@0 163 actors = actors.concat(imports);
michael@0 164 }
michael@0 165 throw new Task.Result(actors);
michael@0 166 }.bind(this));
michael@0 167 },
michael@0 168
michael@0 169 /**
michael@0 170 * Get all the stylesheets @imported from a stylesheet.
michael@0 171 *
michael@0 172 * @param {DOMStyleSheet} styleSheet
michael@0 173 * Style sheet to search
michael@0 174 * @return {Promise}
michael@0 175 * A promise that resolves with an array of StyleSheetActors
michael@0 176 */
michael@0 177 _getImported: function(styleSheet) {
michael@0 178 return Task.spawn(function() {
michael@0 179 let rules = yield styleSheet.getCSSRules();
michael@0 180 let imported = [];
michael@0 181
michael@0 182 for (let i = 0; i < rules.length; i++) {
michael@0 183 let rule = rules[i];
michael@0 184 if (rule.type == Ci.nsIDOMCSSRule.IMPORT_RULE) {
michael@0 185 // Associated styleSheet may be null if it has already been seen due
michael@0 186 // to duplicate @imports for the same URL.
michael@0 187 if (!rule.styleSheet) {
michael@0 188 continue;
michael@0 189 }
michael@0 190 let actor = this._createStyleSheetActor(rule.styleSheet);
michael@0 191 imported.push(actor);
michael@0 192
michael@0 193 // recurse imports in this stylesheet as well
michael@0 194 let children = yield this._getImported(actor);
michael@0 195 imported = imported.concat(children);
michael@0 196 }
michael@0 197 else if (rule.type != Ci.nsIDOMCSSRule.CHARSET_RULE) {
michael@0 198 // @import rules must precede all others except @charset
michael@0 199 break;
michael@0 200 }
michael@0 201 }
michael@0 202
michael@0 203 throw new Task.Result(imported);
michael@0 204 }.bind(this));
michael@0 205 },
michael@0 206
michael@0 207 /**
michael@0 208 * Create a new actor for a style sheet, if it hasn't already been created.
michael@0 209 *
michael@0 210 * @param {DOMStyleSheet} styleSheet
michael@0 211 * The style sheet to create an actor for.
michael@0 212 * @return {StyleSheetActor}
michael@0 213 * The actor for this style sheet
michael@0 214 */
michael@0 215 _createStyleSheetActor: function(styleSheet)
michael@0 216 {
michael@0 217 if (this._sheets.has(styleSheet)) {
michael@0 218 return this._sheets.get(styleSheet);
michael@0 219 }
michael@0 220 let actor = new StyleSheetActor(styleSheet, this);
michael@0 221
michael@0 222 this.manage(actor);
michael@0 223 this._sheets.set(styleSheet, actor);
michael@0 224
michael@0 225 return actor;
michael@0 226 },
michael@0 227
michael@0 228 /**
michael@0 229 * Clear all the current stylesheet actors in map.
michael@0 230 */
michael@0 231 _clearStyleSheetActors: function() {
michael@0 232 for (let actor in this._sheets) {
michael@0 233 this.unmanage(this._sheets[actor]);
michael@0 234 }
michael@0 235 this._sheets.clear();
michael@0 236 },
michael@0 237
michael@0 238 /**
michael@0 239 * Create a new style sheet in the document with the given text.
michael@0 240 * Return an actor for it.
michael@0 241 *
michael@0 242 * @param {object} request
michael@0 243 * Debugging protocol request object, with 'text property'
michael@0 244 * @return {object}
michael@0 245 * Object with 'styelSheet' property for form on new actor.
michael@0 246 */
michael@0 247 addStyleSheet: method(function(text) {
michael@0 248 let parent = this.document.documentElement;
michael@0 249 let style = this.document.createElementNS("http://www.w3.org/1999/xhtml", "style");
michael@0 250 style.setAttribute("type", "text/css");
michael@0 251
michael@0 252 if (text) {
michael@0 253 style.appendChild(this.document.createTextNode(text));
michael@0 254 }
michael@0 255 parent.appendChild(style);
michael@0 256
michael@0 257 let actor = this._createStyleSheetActor(style.sheet);
michael@0 258 return actor;
michael@0 259 }, {
michael@0 260 request: { text: Arg(0, "string") },
michael@0 261 response: { styleSheet: RetVal("stylesheet") }
michael@0 262 })
michael@0 263 });
michael@0 264
michael@0 265 /**
michael@0 266 * The corresponding Front object for the StyleSheetsActor.
michael@0 267 */
michael@0 268 let StyleSheetsFront = protocol.FrontClass(StyleSheetsActor, {
michael@0 269 initialize: function(client, tabForm) {
michael@0 270 protocol.Front.prototype.initialize.call(this, client);
michael@0 271 this.actorID = tabForm.styleSheetsActor;
michael@0 272
michael@0 273 client.addActorPool(this);
michael@0 274 this.manage(this);
michael@0 275 }
michael@0 276 });
michael@0 277
michael@0 278 /**
michael@0 279 * A StyleSheetActor represents a stylesheet on the server.
michael@0 280 */
michael@0 281 let StyleSheetActor = protocol.ActorClass({
michael@0 282 typeName: "stylesheet",
michael@0 283
michael@0 284 events: {
michael@0 285 "property-change" : {
michael@0 286 type: "propertyChange",
michael@0 287 property: Arg(0, "string"),
michael@0 288 value: Arg(1, "json")
michael@0 289 },
michael@0 290 "style-applied" : {
michael@0 291 type: "styleApplied"
michael@0 292 }
michael@0 293 },
michael@0 294
michael@0 295 /* List of original sources that generated this stylesheet */
michael@0 296 _originalSources: null,
michael@0 297
michael@0 298 toString: function() {
michael@0 299 return "[StyleSheetActor " + this.actorID + "]";
michael@0 300 },
michael@0 301
michael@0 302 /**
michael@0 303 * Window of target
michael@0 304 */
michael@0 305 get window() this._window || this.parentActor.window,
michael@0 306
michael@0 307 /**
michael@0 308 * Document of target.
michael@0 309 */
michael@0 310 get document() this.window.document,
michael@0 311
michael@0 312 /**
michael@0 313 * URL of underlying stylesheet.
michael@0 314 */
michael@0 315 get href() this.rawSheet.href,
michael@0 316
michael@0 317 /**
michael@0 318 * Retrieve the index (order) of stylesheet in the document.
michael@0 319 *
michael@0 320 * @return number
michael@0 321 */
michael@0 322 get styleSheetIndex()
michael@0 323 {
michael@0 324 if (this._styleSheetIndex == -1) {
michael@0 325 for (let i = 0; i < this.document.styleSheets.length; i++) {
michael@0 326 if (this.document.styleSheets[i] == this.rawSheet) {
michael@0 327 this._styleSheetIndex = i;
michael@0 328 break;
michael@0 329 }
michael@0 330 }
michael@0 331 }
michael@0 332 return this._styleSheetIndex;
michael@0 333 },
michael@0 334
michael@0 335 initialize: function(aStyleSheet, aParentActor, aWindow) {
michael@0 336 protocol.Actor.prototype.initialize.call(this, null);
michael@0 337
michael@0 338 this.rawSheet = aStyleSheet;
michael@0 339 this.parentActor = aParentActor;
michael@0 340 this.conn = this.parentActor.conn;
michael@0 341
michael@0 342 this._window = aWindow;
michael@0 343
michael@0 344 // text and index are unknown until source load
michael@0 345 this.text = null;
michael@0 346 this._styleSheetIndex = -1;
michael@0 347
michael@0 348 this._transitionRefCount = 0;
michael@0 349 },
michael@0 350
michael@0 351 /**
michael@0 352 * Get the raw stylesheet's cssRules once the sheet has been loaded.
michael@0 353 *
michael@0 354 * @return {Promise}
michael@0 355 * Promise that resolves with a CSSRuleList
michael@0 356 */
michael@0 357 getCSSRules: function() {
michael@0 358 let rules;
michael@0 359 try {
michael@0 360 rules = this.rawSheet.cssRules;
michael@0 361 }
michael@0 362 catch (e) {
michael@0 363 // sheet isn't loaded yet
michael@0 364 }
michael@0 365
michael@0 366 if (rules) {
michael@0 367 return promise.resolve(rules);
michael@0 368 }
michael@0 369
michael@0 370 let ownerNode = this.rawSheet.ownerNode;
michael@0 371 if (!ownerNode) {
michael@0 372 return promise.resolve([]);
michael@0 373 }
michael@0 374
michael@0 375 if (this._cssRules) {
michael@0 376 return this._cssRules;
michael@0 377 }
michael@0 378
michael@0 379 let deferred = promise.defer();
michael@0 380
michael@0 381 let onSheetLoaded = function(event) {
michael@0 382 ownerNode.removeEventListener("load", onSheetLoaded, false);
michael@0 383
michael@0 384 deferred.resolve(this.rawSheet.cssRules);
michael@0 385 }.bind(this);
michael@0 386
michael@0 387 ownerNode.addEventListener("load", onSheetLoaded, false);
michael@0 388
michael@0 389 // cache so we don't add many listeners if this is called multiple times.
michael@0 390 this._cssRules = deferred.promise;
michael@0 391
michael@0 392 return this._cssRules;
michael@0 393 },
michael@0 394
michael@0 395 /**
michael@0 396 * Get the current state of the actor
michael@0 397 *
michael@0 398 * @return {object}
michael@0 399 * With properties of the underlying stylesheet, plus 'text',
michael@0 400 * 'styleSheetIndex' and 'parentActor' if it's @imported
michael@0 401 */
michael@0 402 form: function(detail) {
michael@0 403 if (detail === "actorid") {
michael@0 404 return this.actorID;
michael@0 405 }
michael@0 406
michael@0 407 let docHref;
michael@0 408 let ownerNode = this.rawSheet.ownerNode;
michael@0 409 if (ownerNode) {
michael@0 410 if (ownerNode instanceof Ci.nsIDOMHTMLDocument) {
michael@0 411 docHref = ownerNode.location.href;
michael@0 412 }
michael@0 413 else if (ownerNode.ownerDocument && ownerNode.ownerDocument.location) {
michael@0 414 docHref = ownerNode.ownerDocument.location.href;
michael@0 415 }
michael@0 416 }
michael@0 417
michael@0 418 let form = {
michael@0 419 actor: this.actorID, // actorID is set when this actor is added to a pool
michael@0 420 href: this.href,
michael@0 421 nodeHref: docHref,
michael@0 422 disabled: this.rawSheet.disabled,
michael@0 423 title: this.rawSheet.title,
michael@0 424 system: !CssLogic.isContentStylesheet(this.rawSheet),
michael@0 425 styleSheetIndex: this.styleSheetIndex
michael@0 426 }
michael@0 427
michael@0 428 try {
michael@0 429 form.ruleCount = this.rawSheet.cssRules.length;
michael@0 430 }
michael@0 431 catch(e) {
michael@0 432 // stylesheet had an @import rule that wasn't loaded yet
michael@0 433 this.getCSSRules().then(() => {
michael@0 434 this._notifyPropertyChanged("ruleCount");
michael@0 435 });
michael@0 436 }
michael@0 437 return form;
michael@0 438 },
michael@0 439
michael@0 440 /**
michael@0 441 * Toggle the disabled property of the style sheet
michael@0 442 *
michael@0 443 * @return {object}
michael@0 444 * 'disabled' - the disabled state after toggling.
michael@0 445 */
michael@0 446 toggleDisabled: method(function() {
michael@0 447 this.rawSheet.disabled = !this.rawSheet.disabled;
michael@0 448 this._notifyPropertyChanged("disabled");
michael@0 449
michael@0 450 return this.rawSheet.disabled;
michael@0 451 }, {
michael@0 452 response: { disabled: RetVal("boolean")}
michael@0 453 }),
michael@0 454
michael@0 455 /**
michael@0 456 * Send an event notifying that a property of the stylesheet
michael@0 457 * has changed.
michael@0 458 *
michael@0 459 * @param {string} property
michael@0 460 * Name of the changed property
michael@0 461 */
michael@0 462 _notifyPropertyChanged: function(property) {
michael@0 463 events.emit(this, "property-change", property, this.form()[property]);
michael@0 464 },
michael@0 465
michael@0 466 /**
michael@0 467 * Protocol method to get the text of this stylesheet.
michael@0 468 */
michael@0 469 getText: method(function() {
michael@0 470 return this._getText().then((text) => {
michael@0 471 return new LongStringActor(this.conn, text || "");
michael@0 472 });
michael@0 473 }, {
michael@0 474 response: {
michael@0 475 text: RetVal("longstring")
michael@0 476 }
michael@0 477 }),
michael@0 478
michael@0 479 /**
michael@0 480 * Fetch the text for this stylesheet from the cache or network. Return
michael@0 481 * cached text if it's already been fetched.
michael@0 482 *
michael@0 483 * @return {Promise}
michael@0 484 * Promise that resolves with a string text of the stylesheet.
michael@0 485 */
michael@0 486 _getText: function() {
michael@0 487 if (this.text) {
michael@0 488 return promise.resolve(this.text);
michael@0 489 }
michael@0 490
michael@0 491 if (!this.href) {
michael@0 492 // this is an inline <style> sheet
michael@0 493 let content = this.rawSheet.ownerNode.textContent;
michael@0 494 this.text = content;
michael@0 495 return promise.resolve(content);
michael@0 496 }
michael@0 497
michael@0 498 let options = {
michael@0 499 window: this.window,
michael@0 500 charset: this._getCSSCharset()
michael@0 501 };
michael@0 502
michael@0 503 return fetch(this.href, options).then(({ content }) => {
michael@0 504 this.text = content;
michael@0 505 return content;
michael@0 506 });
michael@0 507 },
michael@0 508
michael@0 509 /**
michael@0 510 * Protocol method to get the original source (actors) for this
michael@0 511 * stylesheet if it has uses source maps.
michael@0 512 */
michael@0 513 getOriginalSources: method(function() {
michael@0 514 if (this._originalSources) {
michael@0 515 return promise.resolve(this._originalSources);
michael@0 516 }
michael@0 517 return this._fetchOriginalSources();
michael@0 518 }, {
michael@0 519 request: {},
michael@0 520 response: {
michael@0 521 originalSources: RetVal("nullable:array:originalsource")
michael@0 522 }
michael@0 523 }),
michael@0 524
michael@0 525 /**
michael@0 526 * Fetch the original sources (actors) for this style sheet using its
michael@0 527 * source map. If they've already been fetched, returns cached array.
michael@0 528 *
michael@0 529 * @return {Promise}
michael@0 530 * Promise that resolves with an array of OriginalSourceActors
michael@0 531 */
michael@0 532 _fetchOriginalSources: function() {
michael@0 533 this._clearOriginalSources();
michael@0 534 this._originalSources = [];
michael@0 535
michael@0 536 return this.getSourceMap().then((sourceMap) => {
michael@0 537 if (!sourceMap) {
michael@0 538 return null;
michael@0 539 }
michael@0 540 for (let url of sourceMap.sources) {
michael@0 541 let actor = new OriginalSourceActor(url, sourceMap, this);
michael@0 542
michael@0 543 this.manage(actor);
michael@0 544 this._originalSources.push(actor);
michael@0 545 }
michael@0 546 return this._originalSources;
michael@0 547 })
michael@0 548 },
michael@0 549
michael@0 550 /**
michael@0 551 * Get the SourceMapConsumer for this stylesheet's source map, if
michael@0 552 * it exists. Saves the consumer for later queries.
michael@0 553 *
michael@0 554 * @return {Promise}
michael@0 555 * A promise that resolves with a SourceMapConsumer, or null.
michael@0 556 */
michael@0 557 getSourceMap: function() {
michael@0 558 if (this._sourceMap) {
michael@0 559 return this._sourceMap;
michael@0 560 }
michael@0 561 return this._fetchSourceMap();
michael@0 562 },
michael@0 563
michael@0 564 /**
michael@0 565 * Fetch the source map for this stylesheet.
michael@0 566 *
michael@0 567 * @return {Promise}
michael@0 568 * A promise that resolves with a SourceMapConsumer, or null.
michael@0 569 */
michael@0 570 _fetchSourceMap: function() {
michael@0 571 let deferred = promise.defer();
michael@0 572
michael@0 573 this._getText().then((content) => {
michael@0 574 let url = this._extractSourceMapUrl(content);
michael@0 575 if (!url) {
michael@0 576 // no source map for this stylesheet
michael@0 577 deferred.resolve(null);
michael@0 578 return;
michael@0 579 };
michael@0 580
michael@0 581 url = normalize(url, this.href);
michael@0 582
michael@0 583 let map = fetch(url, { loadFromCache: false, window: this.window })
michael@0 584 .then(({content}) => {
michael@0 585 let map = new SourceMapConsumer(content);
michael@0 586 this._setSourceMapRoot(map, url, this.href);
michael@0 587 this._sourceMap = promise.resolve(map);
michael@0 588
michael@0 589 deferred.resolve(map);
michael@0 590 return map;
michael@0 591 }, deferred.reject);
michael@0 592
michael@0 593 this._sourceMap = map;
michael@0 594 }, deferred.reject);
michael@0 595
michael@0 596 return deferred.promise;
michael@0 597 },
michael@0 598
michael@0 599 /**
michael@0 600 * Clear and unmanage the original source actors for this stylesheet.
michael@0 601 */
michael@0 602 _clearOriginalSources: function() {
michael@0 603 for (actor in this._originalSources) {
michael@0 604 this.unmanage(actor);
michael@0 605 }
michael@0 606 this._originalSources = null;
michael@0 607 },
michael@0 608
michael@0 609 /**
michael@0 610 * Sets the source map's sourceRoot to be relative to the source map url.
michael@0 611 */
michael@0 612 _setSourceMapRoot: function(aSourceMap, aAbsSourceMapURL, aScriptURL) {
michael@0 613 const base = dirname(
michael@0 614 aAbsSourceMapURL.startsWith("data:")
michael@0 615 ? aScriptURL
michael@0 616 : aAbsSourceMapURL);
michael@0 617 aSourceMap.sourceRoot = aSourceMap.sourceRoot
michael@0 618 ? normalize(aSourceMap.sourceRoot, base)
michael@0 619 : base;
michael@0 620 },
michael@0 621
michael@0 622 /**
michael@0 623 * Get the source map url specified in the text of a stylesheet.
michael@0 624 *
michael@0 625 * @param {string} content
michael@0 626 * The text of the style sheet.
michael@0 627 * @return {string}
michael@0 628 * Url of source map.
michael@0 629 */
michael@0 630 _extractSourceMapUrl: function(content) {
michael@0 631 var matches = /sourceMappingURL\=([^\s\*]*)/.exec(content);
michael@0 632 if (matches) {
michael@0 633 return matches[1];
michael@0 634 }
michael@0 635 return null;
michael@0 636 },
michael@0 637
michael@0 638 /**
michael@0 639 * Protocol method that gets the location in the original source of a
michael@0 640 * line, column pair in this stylesheet, if its source mapped, otherwise
michael@0 641 * a promise of the same location.
michael@0 642 */
michael@0 643 getOriginalLocation: method(function(line, column) {
michael@0 644 return this.getSourceMap().then((sourceMap) => {
michael@0 645 if (sourceMap) {
michael@0 646 return sourceMap.originalPositionFor({ line: line, column: column });
michael@0 647 }
michael@0 648 return {
michael@0 649 source: this.href,
michael@0 650 line: line,
michael@0 651 column: column
michael@0 652 }
michael@0 653 });
michael@0 654 }, {
michael@0 655 request: {
michael@0 656 line: Arg(0, "number"),
michael@0 657 column: Arg(1, "number")
michael@0 658 },
michael@0 659 response: RetVal(types.addDictType("originallocationresponse", {
michael@0 660 source: "string",
michael@0 661 line: "number",
michael@0 662 column: "number"
michael@0 663 }))
michael@0 664 }),
michael@0 665
michael@0 666 /**
michael@0 667 * Get the charset of the stylesheet according to the character set rules
michael@0 668 * defined in <http://www.w3.org/TR/CSS2/syndata.html#charset>.
michael@0 669 *
michael@0 670 * @param string channelCharset
michael@0 671 * Charset of the source string if set by the HTTP channel.
michael@0 672 */
michael@0 673 _getCSSCharset: function(channelCharset)
michael@0 674 {
michael@0 675 // StyleSheet's charset can be specified from multiple sources
michael@0 676 if (channelCharset && channelCharset.length > 0) {
michael@0 677 // step 1 of syndata.html: charset given in HTTP header.
michael@0 678 return channelCharset;
michael@0 679 }
michael@0 680
michael@0 681 let sheet = this.rawSheet;
michael@0 682 if (sheet) {
michael@0 683 // Do we have a @charset rule in the stylesheet?
michael@0 684 // step 2 of syndata.html (without the BOM check).
michael@0 685 if (sheet.cssRules) {
michael@0 686 let rules = sheet.cssRules;
michael@0 687 if (rules.length
michael@0 688 && rules.item(0).type == Ci.nsIDOMCSSRule.CHARSET_RULE) {
michael@0 689 return rules.item(0).encoding;
michael@0 690 }
michael@0 691 }
michael@0 692
michael@0 693 // step 3: charset attribute of <link> or <style> element, if it exists
michael@0 694 if (sheet.ownerNode && sheet.ownerNode.getAttribute) {
michael@0 695 let linkCharset = sheet.ownerNode.getAttribute("charset");
michael@0 696 if (linkCharset != null) {
michael@0 697 return linkCharset;
michael@0 698 }
michael@0 699 }
michael@0 700
michael@0 701 // step 4 (1 of 2): charset of referring stylesheet.
michael@0 702 let parentSheet = sheet.parentStyleSheet;
michael@0 703 if (parentSheet && parentSheet.cssRules &&
michael@0 704 parentSheet.cssRules[0].type == Ci.nsIDOMCSSRule.CHARSET_RULE) {
michael@0 705 return parentSheet.cssRules[0].encoding;
michael@0 706 }
michael@0 707
michael@0 708 // step 4 (2 of 2): charset of referring document.
michael@0 709 if (sheet.ownerNode && sheet.ownerNode.ownerDocument.characterSet) {
michael@0 710 return sheet.ownerNode.ownerDocument.characterSet;
michael@0 711 }
michael@0 712 }
michael@0 713
michael@0 714 // step 5: default to utf-8.
michael@0 715 return "UTF-8";
michael@0 716 },
michael@0 717
michael@0 718 /**
michael@0 719 * Update the style sheet in place with new text.
michael@0 720 *
michael@0 721 * @param {object} request
michael@0 722 * 'text' - new text
michael@0 723 * 'transition' - whether to do CSS transition for change.
michael@0 724 */
michael@0 725 update: method(function(text, transition) {
michael@0 726 DOMUtils.parseStyleSheet(this.rawSheet, text);
michael@0 727
michael@0 728 this.text = text;
michael@0 729
michael@0 730 this._notifyPropertyChanged("ruleCount");
michael@0 731
michael@0 732 if (transition) {
michael@0 733 this._insertTransistionRule();
michael@0 734 }
michael@0 735 else {
michael@0 736 this._notifyStyleApplied();
michael@0 737 }
michael@0 738 }, {
michael@0 739 request: {
michael@0 740 text: Arg(0, "string"),
michael@0 741 transition: Arg(1, "boolean")
michael@0 742 }
michael@0 743 }),
michael@0 744
michael@0 745 /**
michael@0 746 * Insert a catch-all transition rule into the document. Set a timeout
michael@0 747 * to remove the rule after a certain time.
michael@0 748 */
michael@0 749 _insertTransistionRule: function() {
michael@0 750 // Insert the global transition rule
michael@0 751 // Use a ref count to make sure we do not add it multiple times.. and remove
michael@0 752 // it only when all pending StyleSheets-generated transitions ended.
michael@0 753 if (this._transitionRefCount == 0) {
michael@0 754 this.rawSheet.insertRule(TRANSITION_RULE, this.rawSheet.cssRules.length);
michael@0 755 this.document.documentElement.classList.add(TRANSITION_CLASS);
michael@0 756 }
michael@0 757
michael@0 758 this._transitionRefCount++;
michael@0 759
michael@0 760 // Set up clean up and commit after transition duration (+buffer)
michael@0 761 // @see _onTransitionEnd
michael@0 762 this.window.setTimeout(this._onTransitionEnd.bind(this),
michael@0 763 TRANSITION_DURATION_MS + TRANSITION_BUFFER_MS);
michael@0 764 },
michael@0 765
michael@0 766 /**
michael@0 767 * This cleans up class and rule added for transition effect and then
michael@0 768 * notifies that the style has been applied.
michael@0 769 */
michael@0 770 _onTransitionEnd: function()
michael@0 771 {
michael@0 772 if (--this._transitionRefCount == 0) {
michael@0 773 this.document.documentElement.classList.remove(TRANSITION_CLASS);
michael@0 774 this.rawSheet.deleteRule(this.rawSheet.cssRules.length - 1);
michael@0 775 }
michael@0 776
michael@0 777 events.emit(this, "style-applied");
michael@0 778 }
michael@0 779 })
michael@0 780
michael@0 781 /**
michael@0 782 * StyleSheetFront is the client-side counterpart to a StyleSheetActor.
michael@0 783 */
michael@0 784 var StyleSheetFront = protocol.FrontClass(StyleSheetActor, {
michael@0 785 initialize: function(conn, form) {
michael@0 786 protocol.Front.prototype.initialize.call(this, conn, form);
michael@0 787
michael@0 788 this._onPropertyChange = this._onPropertyChange.bind(this);
michael@0 789 events.on(this, "property-change", this._onPropertyChange);
michael@0 790 },
michael@0 791
michael@0 792 destroy: function() {
michael@0 793 events.off(this, "property-change", this._onPropertyChange);
michael@0 794
michael@0 795 protocol.Front.prototype.destroy.call(this);
michael@0 796 },
michael@0 797
michael@0 798 _onPropertyChange: function(property, value) {
michael@0 799 this._form[property] = value;
michael@0 800 },
michael@0 801
michael@0 802 form: function(form, detail) {
michael@0 803 if (detail === "actorid") {
michael@0 804 this.actorID = form;
michael@0 805 return;
michael@0 806 }
michael@0 807 this.actorID = form.actor;
michael@0 808 this._form = form;
michael@0 809 },
michael@0 810
michael@0 811 get href() this._form.href,
michael@0 812 get nodeHref() this._form.nodeHref,
michael@0 813 get disabled() !!this._form.disabled,
michael@0 814 get title() this._form.title,
michael@0 815 get isSystem() this._form.system,
michael@0 816 get styleSheetIndex() this._form.styleSheetIndex,
michael@0 817 get ruleCount() this._form.ruleCount
michael@0 818 });
michael@0 819
michael@0 820 /**
michael@0 821 * Actor representing an original source of a style sheet that was specified
michael@0 822 * in a source map.
michael@0 823 */
michael@0 824 let OriginalSourceActor = protocol.ActorClass({
michael@0 825 typeName: "originalsource",
michael@0 826
michael@0 827 initialize: function(aUrl, aSourceMap, aParentActor) {
michael@0 828 protocol.Actor.prototype.initialize.call(this, null);
michael@0 829
michael@0 830 this.url = aUrl;
michael@0 831 this.sourceMap = aSourceMap;
michael@0 832 this.parentActor = aParentActor;
michael@0 833 this.conn = this.parentActor.conn;
michael@0 834
michael@0 835 this.text = null;
michael@0 836 },
michael@0 837
michael@0 838 form: function() {
michael@0 839 return {
michael@0 840 actor: this.actorID, // actorID is set when it's added to a pool
michael@0 841 url: this.url,
michael@0 842 relatedStyleSheet: this.parentActor.form()
michael@0 843 };
michael@0 844 },
michael@0 845
michael@0 846 _getText: function() {
michael@0 847 if (this.text) {
michael@0 848 return promise.resolve(this.text);
michael@0 849 }
michael@0 850 let content = this.sourceMap.sourceContentFor(this.url);
michael@0 851 if (content) {
michael@0 852 this.text = content;
michael@0 853 return promise.resolve(content);
michael@0 854 }
michael@0 855 return fetch(this.url, { window: this.window }).then(({content}) => {
michael@0 856 this.text = content;
michael@0 857 return content;
michael@0 858 });
michael@0 859 },
michael@0 860
michael@0 861 /**
michael@0 862 * Protocol method to get the text of this source.
michael@0 863 */
michael@0 864 getText: method(function() {
michael@0 865 return this._getText().then((text) => {
michael@0 866 return new LongStringActor(this.conn, text || "");
michael@0 867 });
michael@0 868 }, {
michael@0 869 response: {
michael@0 870 text: RetVal("longstring")
michael@0 871 }
michael@0 872 })
michael@0 873 })
michael@0 874
michael@0 875 /**
michael@0 876 * The client-side counterpart for an OriginalSourceActor.
michael@0 877 */
michael@0 878 let OriginalSourceFront = protocol.FrontClass(OriginalSourceActor, {
michael@0 879 initialize: function(client, form) {
michael@0 880 protocol.Front.prototype.initialize.call(this, client, form);
michael@0 881
michael@0 882 this.isOriginalSource = true;
michael@0 883 },
michael@0 884
michael@0 885 form: function(form, detail) {
michael@0 886 if (detail === "actorid") {
michael@0 887 this.actorID = form;
michael@0 888 return;
michael@0 889 }
michael@0 890 this.actorID = form.actor;
michael@0 891 this._form = form;
michael@0 892 },
michael@0 893
michael@0 894 get href() this._form.url,
michael@0 895 get url() this._form.url
michael@0 896 });
michael@0 897
michael@0 898
michael@0 899 XPCOMUtils.defineLazyGetter(this, "DOMUtils", function () {
michael@0 900 return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
michael@0 901 });
michael@0 902
michael@0 903 exports.StyleSheetsActor = StyleSheetsActor;
michael@0 904 exports.StyleSheetsFront = StyleSheetsFront;
michael@0 905
michael@0 906 exports.StyleSheetActor = StyleSheetActor;
michael@0 907 exports.StyleSheetFront = StyleSheetFront;
michael@0 908
michael@0 909
michael@0 910 /**
michael@0 911 * Performs a request to load the desired URL and returns a promise.
michael@0 912 *
michael@0 913 * @param aURL String
michael@0 914 * The URL we will request.
michael@0 915 * @returns Promise
michael@0 916 * A promise of the document at that URL, as a string.
michael@0 917 */
michael@0 918 function fetch(aURL, aOptions={ loadFromCache: true, window: null,
michael@0 919 charset: null}) {
michael@0 920 let deferred = promise.defer();
michael@0 921 let scheme;
michael@0 922 let url = aURL.split(" -> ").pop();
michael@0 923 let charset;
michael@0 924 let contentType;
michael@0 925
michael@0 926 try {
michael@0 927 scheme = Services.io.extractScheme(url);
michael@0 928 } catch (e) {
michael@0 929 // In the xpcshell tests, the script url is the absolute path of the test
michael@0 930 // file, which will make a malformed URI error be thrown. Add the file
michael@0 931 // scheme prefix ourselves.
michael@0 932 url = "file://" + url;
michael@0 933 scheme = Services.io.extractScheme(url);
michael@0 934 }
michael@0 935
michael@0 936 switch (scheme) {
michael@0 937 case "file":
michael@0 938 case "chrome":
michael@0 939 case "resource":
michael@0 940 try {
michael@0 941 NetUtil.asyncFetch(url, function onFetch(aStream, aStatus, aRequest) {
michael@0 942 if (!components.isSuccessCode(aStatus)) {
michael@0 943 deferred.reject(new Error("Request failed with status code = "
michael@0 944 + aStatus
michael@0 945 + " after NetUtil.asyncFetch for url = "
michael@0 946 + url));
michael@0 947 return;
michael@0 948 }
michael@0 949
michael@0 950 let source = NetUtil.readInputStreamToString(aStream, aStream.available());
michael@0 951 contentType = aRequest.contentType;
michael@0 952 deferred.resolve(source);
michael@0 953 aStream.close();
michael@0 954 });
michael@0 955 } catch (ex) {
michael@0 956 deferred.reject(ex);
michael@0 957 }
michael@0 958 break;
michael@0 959
michael@0 960 default:
michael@0 961 let channel;
michael@0 962 try {
michael@0 963 channel = Services.io.newChannel(url, null, null);
michael@0 964 } catch (e if e.name == "NS_ERROR_UNKNOWN_PROTOCOL") {
michael@0 965 // On Windows xpcshell tests, c:/foo/bar can pass as a valid URL, but
michael@0 966 // newChannel won't be able to handle it.
michael@0 967 url = "file:///" + url;
michael@0 968 channel = Services.io.newChannel(url, null, null);
michael@0 969 }
michael@0 970 let chunks = [];
michael@0 971 let streamListener = {
michael@0 972 onStartRequest: function(aRequest, aContext, aStatusCode) {
michael@0 973 if (!components.isSuccessCode(aStatusCode)) {
michael@0 974 deferred.reject(new Error("Request failed with status code = "
michael@0 975 + aStatusCode
michael@0 976 + " in onStartRequest handler for url = "
michael@0 977 + url));
michael@0 978 }
michael@0 979 },
michael@0 980 onDataAvailable: function(aRequest, aContext, aStream, aOffset, aCount) {
michael@0 981 chunks.push(NetUtil.readInputStreamToString(aStream, aCount));
michael@0 982 },
michael@0 983 onStopRequest: function(aRequest, aContext, aStatusCode) {
michael@0 984 if (!components.isSuccessCode(aStatusCode)) {
michael@0 985 deferred.reject(new Error("Request failed with status code = "
michael@0 986 + aStatusCode
michael@0 987 + " in onStopRequest handler for url = "
michael@0 988 + url));
michael@0 989 return;
michael@0 990 }
michael@0 991
michael@0 992 charset = channel.contentCharset || charset;
michael@0 993 contentType = channel.contentType;
michael@0 994 deferred.resolve(chunks.join(""));
michael@0 995 }
michael@0 996 };
michael@0 997
michael@0 998 if (aOptions.window) {
michael@0 999 // respect private browsing
michael@0 1000 channel.loadGroup = aOptions.window.QueryInterface(Ci.nsIInterfaceRequestor)
michael@0 1001 .getInterface(Ci.nsIWebNavigation)
michael@0 1002 .QueryInterface(Ci.nsIDocumentLoader)
michael@0 1003 .loadGroup;
michael@0 1004 }
michael@0 1005 channel.loadFlags = aOptions.loadFromCache
michael@0 1006 ? channel.LOAD_FROM_CACHE
michael@0 1007 : channel.LOAD_BYPASS_CACHE;
michael@0 1008 channel.asyncOpen(streamListener, null);
michael@0 1009 break;
michael@0 1010 }
michael@0 1011
michael@0 1012 return deferred.promise.then(source => {
michael@0 1013 return {
michael@0 1014 content: convertToUnicode(source, charset),
michael@0 1015 contentType: contentType
michael@0 1016 };
michael@0 1017 });
michael@0 1018 }
michael@0 1019
michael@0 1020 /**
michael@0 1021 * Convert a given string, encoded in a given character set, to unicode.
michael@0 1022 *
michael@0 1023 * @param string aString
michael@0 1024 * A string.
michael@0 1025 * @param string aCharset
michael@0 1026 * A character set.
michael@0 1027 */
michael@0 1028 function convertToUnicode(aString, aCharset=null) {
michael@0 1029 // Decoding primitives.
michael@0 1030 let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
michael@0 1031 .createInstance(Ci.nsIScriptableUnicodeConverter);
michael@0 1032 try {
michael@0 1033 converter.charset = aCharset || "UTF-8";
michael@0 1034 return converter.ConvertToUnicode(aString);
michael@0 1035 } catch(e) {
michael@0 1036 return aString;
michael@0 1037 }
michael@0 1038 }
michael@0 1039
michael@0 1040 /**
michael@0 1041 * Normalize multiple relative paths towards the base paths on the right.
michael@0 1042 */
michael@0 1043 function normalize(...aURLs) {
michael@0 1044 let base = Services.io.newURI(aURLs.pop(), null, null);
michael@0 1045 let url;
michael@0 1046 while ((url = aURLs.pop())) {
michael@0 1047 base = Services.io.newURI(url, null, base);
michael@0 1048 }
michael@0 1049 return base.spec;
michael@0 1050 }
michael@0 1051
michael@0 1052 function dirname(aPath) {
michael@0 1053 return Services.io.newURI(
michael@0 1054 ".", null, Services.io.newURI(aPath, null, null)).spec;
michael@0 1055 }

mercurial