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