diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /devtools/server/actors/stylesheets.js | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip |
Add m-esr52 at 52.6.0
Diffstat (limited to 'devtools/server/actors/stylesheets.js')
-rw-r--r-- | devtools/server/actors/stylesheets.js | 982 |
1 files changed, 982 insertions, 0 deletions
diff --git a/devtools/server/actors/stylesheets.js b/devtools/server/actors/stylesheets.js new file mode 100644 index 000000000..f20634e6c --- /dev/null +++ b/devtools/server/actors/stylesheets.js @@ -0,0 +1,982 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const {Cc, Ci} = require("chrome"); +const Services = require("Services"); +const {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm"); +const promise = require("promise"); +const {Task} = require("devtools/shared/task"); +const events = require("sdk/event/core"); +const protocol = require("devtools/shared/protocol"); +const {LongStringActor} = require("devtools/server/actors/string"); +const {fetch} = require("devtools/shared/DevToolsUtils"); +const {listenOnce} = require("devtools/shared/async-utils"); +const {originalSourceSpec, mediaRuleSpec, styleSheetSpec, + styleSheetsSpec} = require("devtools/shared/specs/stylesheets"); +const {SourceMapConsumer} = require("source-map"); +const { installHelperSheet, + addPseudoClassLock, removePseudoClassLock } = require("devtools/server/actors/highlighters/utils/markup"); + +loader.lazyGetter(this, "CssLogic", () => require("devtools/shared/inspector/css-logic")); + +XPCOMUtils.defineLazyGetter(this, "DOMUtils", function () { + return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils); +}); + +var TRANSITION_PSEUDO_CLASS = ":-moz-styleeditor-transitioning"; +var TRANSITION_DURATION_MS = 500; +var TRANSITION_BUFFER_MS = 1000; +var TRANSITION_RULE_SELECTOR = +`:root${TRANSITION_PSEUDO_CLASS}, :root${TRANSITION_PSEUDO_CLASS} *`; +var TRANSITION_RULE = `${TRANSITION_RULE_SELECTOR} { + transition-duration: ${TRANSITION_DURATION_MS}ms !important; + transition-delay: 0ms !important; + transition-timing-function: ease-out !important; + transition-property: all !important; +}`; + +var LOAD_ERROR = "error-load"; + +// The possible kinds of style-applied events. +// UPDATE_PRESERVING_RULES means that the update is guaranteed to +// preserve the number and order of rules on the style sheet. +// UPDATE_GENERAL covers any other kind of change to the style sheet. +const UPDATE_PRESERVING_RULES = 0; +exports.UPDATE_PRESERVING_RULES = UPDATE_PRESERVING_RULES; +const UPDATE_GENERAL = 1; +exports.UPDATE_GENERAL = UPDATE_GENERAL; + +// If the user edits a style sheet, we stash a copy of the edited text +// here, keyed by the style sheet. This way, if the tools are closed +// and then reopened, the edited text will be available. A weak map +// is used so that navigation by the user will eventually cause the +// edited text to be collected. +let modifiedStyleSheets = new WeakMap(); + +/** + * Actor representing an original source of a style sheet that was specified + * in a source map. + */ +var OriginalSourceActor = protocol.ActorClassWithSpec(originalSourceSpec, { + initialize: function (aUrl, aSourceMap, aParentActor) { + protocol.Actor.prototype.initialize.call(this, null); + + this.url = aUrl; + this.sourceMap = aSourceMap; + this.parentActor = aParentActor; + this.conn = this.parentActor.conn; + + this.text = null; + }, + + form: function () { + return { + actor: this.actorID, // actorID is set when it's added to a pool + url: this.url, + relatedStyleSheet: this.parentActor.form() + }; + }, + + _getText: function () { + if (this.text) { + return promise.resolve(this.text); + } + let content = this.sourceMap.sourceContentFor(this.url); + if (content) { + this.text = content; + return promise.resolve(content); + } + let options = { + policy: Ci.nsIContentPolicy.TYPE_INTERNAL_STYLESHEET, + window: this.window + }; + return fetch(this.url, options).then(({content}) => { + this.text = content; + return content; + }); + }, + + /** + * Protocol method to get the text of this source. + */ + getText: function () { + return this._getText().then((text) => { + return new LongStringActor(this.conn, text || ""); + }); + } +}); + +/** + * A MediaRuleActor lives on the server and provides access to properties + * of a DOM @media rule and emits events when it changes. + */ +var MediaRuleActor = protocol.ActorClassWithSpec(mediaRuleSpec, { + get window() { + return this.parentActor.window; + }, + + get document() { + return this.window.document; + }, + + get matches() { + return this.mql ? this.mql.matches : null; + }, + + initialize: function (aMediaRule, aParentActor) { + protocol.Actor.prototype.initialize.call(this, null); + + this.rawRule = aMediaRule; + this.parentActor = aParentActor; + this.conn = this.parentActor.conn; + + this._matchesChange = this._matchesChange.bind(this); + + this.line = DOMUtils.getRuleLine(aMediaRule); + this.column = DOMUtils.getRuleColumn(aMediaRule); + + try { + this.mql = this.window.matchMedia(aMediaRule.media.mediaText); + } catch (e) { + } + + if (this.mql) { + this.mql.addListener(this._matchesChange); + } + }, + + destroy: function () + { + if (this.mql) { + this.mql.removeListener(this._matchesChange); + } + + protocol.Actor.prototype.destroy.call(this); + }, + + form: function (detail) { + if (detail === "actorid") { + return this.actorID; + } + + let form = { + actor: this.actorID, // actorID is set when this is added to a pool + mediaText: this.rawRule.media.mediaText, + conditionText: this.rawRule.conditionText, + matches: this.matches, + line: this.line, + column: this.column, + parentStyleSheet: this.parentActor.actorID + }; + + return form; + }, + + _matchesChange: function () { + events.emit(this, "matches-change", this.matches); + } +}); + +/** + * A StyleSheetActor represents a stylesheet on the server. + */ +var StyleSheetActor = protocol.ActorClassWithSpec(styleSheetSpec, { + /* List of original sources that generated this stylesheet */ + _originalSources: null, + + toString: function () { + return "[StyleSheetActor " + this.actorID + "]"; + }, + + /** + * Window of target + */ + get window() { + return this._window || this.parentActor.window; + }, + + /** + * Document of target. + */ + get document() { + return this.window.document; + }, + + get ownerNode() { + return this.rawSheet.ownerNode; + }, + + /** + * URL of underlying stylesheet. + */ + get href() { + return this.rawSheet.href; + }, + + /** + * Returns the stylesheet href or the document href if the sheet is inline. + */ + get safeHref() { + let href = this.href; + if (!href) { + if (this.ownerNode instanceof Ci.nsIDOMHTMLDocument) { + href = this.ownerNode.location.href; + } else if (this.ownerNode.ownerDocument && + this.ownerNode.ownerDocument.location) { + href = this.ownerNode.ownerDocument.location.href; + } + } + return href; + }, + + /** + * Retrieve the index (order) of stylesheet in the document. + * + * @return number + */ + get styleSheetIndex() + { + if (this._styleSheetIndex == -1) { + for (let i = 0; i < this.document.styleSheets.length; i++) { + if (this.document.styleSheets[i] == this.rawSheet) { + this._styleSheetIndex = i; + break; + } + } + } + return this._styleSheetIndex; + }, + + destroy: function () { + if (this._transitionTimeout) { + this.window.clearTimeout(this._transitionTimeout); + removePseudoClassLock( + this.document.documentElement, TRANSITION_PSEUDO_CLASS); + } + }, + + /** + * Since StyleSheetActor doesn't have a protocol.js parent actor that take + * care of its lifetime, implementing disconnect is required to cleanup. + */ + disconnect: function () { + this.destroy(); + }, + + initialize: function (aStyleSheet, aParentActor, aWindow) { + protocol.Actor.prototype.initialize.call(this, null); + + this.rawSheet = aStyleSheet; + this.parentActor = aParentActor; + this.conn = this.parentActor.conn; + + this._window = aWindow; + + // text and index are unknown until source load + this.text = null; + this._styleSheetIndex = -1; + }, + + /** + * Test whether all the rules in this sheet have associated source. + * @return {Boolean} true if all the rules have source; false if + * some rule was created via CSSOM. + */ + allRulesHaveSource: function () { + let rules; + try { + rules = this.rawSheet.cssRules; + } catch (e) { + // sheet isn't loaded yet + return true; + } + + for (let i = 0; i < rules.length; i++) { + let rule = rules[i]; + if (DOMUtils.getRelativeRuleLine(rule) === 0) { + return false; + } + } + + return true; + }, + + /** + * Get the raw stylesheet's cssRules once the sheet has been loaded. + * + * @return {Promise} + * Promise that resolves with a CSSRuleList + */ + getCSSRules: function () { + let rules; + try { + rules = this.rawSheet.cssRules; + } + catch (e) { + // sheet isn't loaded yet + } + + if (rules) { + return promise.resolve(rules); + } + + if (!this.ownerNode) { + return promise.resolve([]); + } + + if (this._cssRules) { + return this._cssRules; + } + + let deferred = promise.defer(); + + let onSheetLoaded = (event) => { + this.ownerNode.removeEventListener("load", onSheetLoaded, false); + + deferred.resolve(this.rawSheet.cssRules); + }; + + this.ownerNode.addEventListener("load", onSheetLoaded, false); + + // cache so we don't add many listeners if this is called multiple times. + this._cssRules = deferred.promise; + + return this._cssRules; + }, + + /** + * Get the current state of the actor + * + * @return {object} + * With properties of the underlying stylesheet, plus 'text', + * 'styleSheetIndex' and 'parentActor' if it's @imported + */ + form: function (detail) { + if (detail === "actorid") { + return this.actorID; + } + + let docHref; + if (this.ownerNode) { + if (this.ownerNode instanceof Ci.nsIDOMHTMLDocument) { + docHref = this.ownerNode.location.href; + } + else if (this.ownerNode.ownerDocument && this.ownerNode.ownerDocument.location) { + docHref = this.ownerNode.ownerDocument.location.href; + } + } + + let form = { + actor: this.actorID, // actorID is set when this actor is added to a pool + href: this.href, + nodeHref: docHref, + disabled: this.rawSheet.disabled, + title: this.rawSheet.title, + system: !CssLogic.isContentStylesheet(this.rawSheet), + styleSheetIndex: this.styleSheetIndex + }; + + try { + form.ruleCount = this.rawSheet.cssRules.length; + } + catch (e) { + // stylesheet had an @import rule that wasn't loaded yet + this.getCSSRules().then(() => { + this._notifyPropertyChanged("ruleCount"); + }); + } + return form; + }, + + /** + * Toggle the disabled property of the style sheet + * + * @return {object} + * 'disabled' - the disabled state after toggling. + */ + toggleDisabled: function () { + this.rawSheet.disabled = !this.rawSheet.disabled; + this._notifyPropertyChanged("disabled"); + + return this.rawSheet.disabled; + }, + + /** + * Send an event notifying that a property of the stylesheet + * has changed. + * + * @param {string} property + * Name of the changed property + */ + _notifyPropertyChanged: function (property) { + events.emit(this, "property-change", property, this.form()[property]); + }, + + /** + * Protocol method to get the text of this stylesheet. + */ + getText: function () { + return this._getText().then((text) => { + return new LongStringActor(this.conn, text || ""); + }); + }, + + /** + * Fetch the text for this stylesheet from the cache or network. Return + * cached text if it's already been fetched. + * + * @return {Promise} + * Promise that resolves with a string text of the stylesheet. + */ + _getText: function () { + if (typeof this.text === "string") { + return promise.resolve(this.text); + } + + let cssText = modifiedStyleSheets.get(this.rawSheet); + if (cssText !== undefined) { + this.text = cssText; + return promise.resolve(cssText); + } + + if (!this.href) { + // this is an inline <style> sheet + let content = this.ownerNode.textContent; + this.text = content; + return promise.resolve(content); + } + + let options = { + loadFromCache: true, + policy: Ci.nsIContentPolicy.TYPE_INTERNAL_STYLESHEET, + charset: this._getCSSCharset() + }; + + // Bug 1282660 - We use the system principal to load the default internal + // stylesheets instead of the content principal since such stylesheets + // require system principal to load. At meanwhile, we strip the loadGroup + // for preventing the assertion of the userContextId mismatching. + // The default internal stylesheets load from the 'resource:' URL. + // Bug 1287607, 1291321 - 'chrome' and 'file' protocols should also be handled in the + // same way. + if (!/^(chrome|file|resource):\/\//.test(this.href)) { + options.window = this.window; + options.principal = this.document.nodePrincipal; + } + + return fetch(this.href, options).then(({ content }) => { + this.text = content; + return content; + }); + }, + + /** + * Protocol method to get the original source (actors) for this + * stylesheet if it has uses source maps. + */ + getOriginalSources: function () { + if (this._originalSources) { + return promise.resolve(this._originalSources); + } + return this._fetchOriginalSources(); + }, + + /** + * Fetch the original sources (actors) for this style sheet using its + * source map. If they've already been fetched, returns cached array. + * + * @return {Promise} + * Promise that resolves with an array of OriginalSourceActors + */ + _fetchOriginalSources: function () { + this._clearOriginalSources(); + this._originalSources = []; + + return this.getSourceMap().then((sourceMap) => { + if (!sourceMap) { + return null; + } + for (let url of sourceMap.sources) { + let actor = new OriginalSourceActor(url, sourceMap, this); + + this.manage(actor); + this._originalSources.push(actor); + } + return this._originalSources; + }); + }, + + /** + * Get the SourceMapConsumer for this stylesheet's source map, if + * it exists. Saves the consumer for later queries. + * + * @return {Promise} + * A promise that resolves with a SourceMapConsumer, or null. + */ + getSourceMap: function () { + if (this._sourceMap) { + return this._sourceMap; + } + return this._fetchSourceMap(); + }, + + /** + * Fetch the source map for this stylesheet. + * + * @return {Promise} + * A promise that resolves with a SourceMapConsumer, or null. + */ + _fetchSourceMap: function () { + let deferred = promise.defer(); + + this._getText().then(sheetContent => { + let url = this._extractSourceMapUrl(sheetContent); + if (!url) { + // no source map for this stylesheet + deferred.resolve(null); + return; + } + + url = normalize(url, this.safeHref); + let options = { + loadFromCache: false, + policy: Ci.nsIContentPolicy.TYPE_INTERNAL_STYLESHEET, + window: this.window + }; + + let map = fetch(url, options).then(({content}) => { + // Fetching the source map might have failed with a 404 or other. When + // this happens, SourceMapConsumer may fail with a JSON.parse error. + let consumer; + try { + consumer = new SourceMapConsumer(content); + } catch (e) { + deferred.reject(new Error( + `Source map at ${url} not found or invalid`)); + return null; + } + this._setSourceMapRoot(consumer, url, this.safeHref); + this._sourceMap = promise.resolve(consumer); + + deferred.resolve(consumer); + return consumer; + }, deferred.reject); + + this._sourceMap = map; + }, deferred.reject); + + return deferred.promise; + }, + + /** + * Clear and unmanage the original source actors for this stylesheet. + */ + _clearOriginalSources: function () { + for (actor in this._originalSources) { + this.unmanage(actor); + } + this._originalSources = null; + }, + + /** + * Sets the source map's sourceRoot to be relative to the source map url. + */ + _setSourceMapRoot: function (aSourceMap, aAbsSourceMapURL, aScriptURL) { + if (aScriptURL.startsWith("blob:")) { + aScriptURL = aScriptURL.replace("blob:", ""); + } + const base = dirname( + aAbsSourceMapURL.startsWith("data:") + ? aScriptURL + : aAbsSourceMapURL); + aSourceMap.sourceRoot = aSourceMap.sourceRoot + ? normalize(aSourceMap.sourceRoot, base) + : base; + }, + + /** + * Get the source map url specified in the text of a stylesheet. + * + * @param {string} content + * The text of the style sheet. + * @return {string} + * Url of source map. + */ + _extractSourceMapUrl: function (content) { + var matches = /sourceMappingURL\=([^\s\*]*)/.exec(content); + if (matches) { + return matches[1]; + } + return null; + }, + + /** + * Protocol method that gets the location in the original source of a + * line, column pair in this stylesheet, if its source mapped, otherwise + * a promise of the same location. + */ + getOriginalLocation: function (line, column) { + return this.getSourceMap().then((sourceMap) => { + if (sourceMap) { + return sourceMap.originalPositionFor({ line: line, column: column }); + } + return { + fromSourceMap: false, + source: this.href, + line: line, + column: column + }; + }); + }, + + /** + * Protocol method to get the media rules for the stylesheet. + */ + getMediaRules: function () { + return this._getMediaRules(); + }, + + /** + * Get all the @media rules in this stylesheet. + * + * @return {promise} + * A promise that resolves with an array of MediaRuleActors. + */ + _getMediaRules: function () { + return this.getCSSRules().then((rules) => { + let mediaRules = []; + for (let i = 0; i < rules.length; i++) { + let rule = rules[i]; + if (rule.type != Ci.nsIDOMCSSRule.MEDIA_RULE) { + continue; + } + let actor = new MediaRuleActor(rule, this); + this.manage(actor); + + mediaRules.push(actor); + } + return mediaRules; + }); + }, + + /** + * Get the charset of the stylesheet according to the character set rules + * defined in <http://www.w3.org/TR/CSS2/syndata.html#charset>. + * Note that some of the algorithm is implemented in DevToolsUtils.fetch. + */ + _getCSSCharset: function () + { + let sheet = this.rawSheet; + if (sheet) { + // Do we have a @charset rule in the stylesheet? + // step 2 of syndata.html (without the BOM check). + if (sheet.cssRules) { + let rules = sheet.cssRules; + if (rules.length + && rules.item(0).type == Ci.nsIDOMCSSRule.CHARSET_RULE) { + return rules.item(0).encoding; + } + } + + // step 3: charset attribute of <link> or <style> element, if it exists + if (sheet.ownerNode && sheet.ownerNode.getAttribute) { + let linkCharset = sheet.ownerNode.getAttribute("charset"); + if (linkCharset != null) { + return linkCharset; + } + } + + // step 4 (1 of 2): charset of referring stylesheet. + let parentSheet = sheet.parentStyleSheet; + if (parentSheet && parentSheet.cssRules && + parentSheet.cssRules[0].type == Ci.nsIDOMCSSRule.CHARSET_RULE) { + return parentSheet.cssRules[0].encoding; + } + + // step 4 (2 of 2): charset of referring document. + if (sheet.ownerNode && sheet.ownerNode.ownerDocument.characterSet) { + return sheet.ownerNode.ownerDocument.characterSet; + } + } + + // step 5: default to utf-8. + return "UTF-8"; + }, + + /** + * Update the style sheet in place with new text. + * + * @param {object} request + * 'text' - new text + * 'transition' - whether to do CSS transition for change. + * 'kind' - either UPDATE_PRESERVING_RULES or UPDATE_GENERAL + */ + update: function (text, transition, kind = UPDATE_GENERAL) { + DOMUtils.parseStyleSheet(this.rawSheet, text); + + modifiedStyleSheets.set(this.rawSheet, text); + + this.text = text; + + this._notifyPropertyChanged("ruleCount"); + + if (transition) { + this._insertTransistionRule(kind); + } + else { + events.emit(this, "style-applied", kind, this); + } + + this._getMediaRules().then((rules) => { + events.emit(this, "media-rules-changed", rules); + }); + }, + + /** + * Insert a catch-all transition rule into the document. Set a timeout + * to remove the rule after a certain time. + */ + _insertTransistionRule: function (kind) { + addPseudoClassLock(this.document.documentElement, TRANSITION_PSEUDO_CLASS); + + // We always add the rule since we've just reset all the rules + this.rawSheet.insertRule(TRANSITION_RULE, this.rawSheet.cssRules.length); + + // Set up clean up and commit after transition duration (+buffer) + // @see _onTransitionEnd + this.window.clearTimeout(this._transitionTimeout); + this._transitionTimeout = this.window.setTimeout(this._onTransitionEnd.bind(this, kind), + TRANSITION_DURATION_MS + TRANSITION_BUFFER_MS); + }, + + /** + * This cleans up class and rule added for transition effect and then + * notifies that the style has been applied. + */ + _onTransitionEnd: function (kind) + { + this._transitionTimeout = null; + removePseudoClassLock(this.document.documentElement, TRANSITION_PSEUDO_CLASS); + + let index = this.rawSheet.cssRules.length - 1; + let rule = this.rawSheet.cssRules[index]; + if (rule.selectorText == TRANSITION_RULE_SELECTOR) { + this.rawSheet.deleteRule(index); + } + + events.emit(this, "style-applied", kind, this); + } +}); + +exports.StyleSheetActor = StyleSheetActor; + +/** + * Creates a StyleSheetsActor. StyleSheetsActor provides remote access to the + * stylesheets of a document. + */ +var StyleSheetsActor = protocol.ActorClassWithSpec(styleSheetsSpec, { + /** + * The window we work with, taken from the parent actor. + */ + get window() { + return this.parentActor.window; + }, + + /** + * The current content document of the window we work with. + */ + get document() { + return this.window.document; + }, + + form: function () + { + return { actor: this.actorID }; + }, + + initialize: function (conn, tabActor) { + protocol.Actor.prototype.initialize.call(this, null); + + this.parentActor = tabActor; + }, + + /** + * Protocol method for getting a list of StyleSheetActors representing + * all the style sheets in this document. + */ + getStyleSheets: Task.async(function* () { + // Iframe document can change during load (bug 1171919). Track their windows + // instead. + let windows = [this.window]; + let actors = []; + + for (let win of windows) { + let sheets = yield this._addStyleSheets(win); + actors = actors.concat(sheets); + + // Recursively handle style sheets of the documents in iframes. + for (let iframe of win.document.querySelectorAll("iframe, browser, frame")) { + if (iframe.contentDocument && iframe.contentWindow) { + // Sometimes, iframes don't have any document, like the + // one that are over deeply nested (bug 285395) + windows.push(iframe.contentWindow); + } + } + } + return actors; + }), + + /** + * Check if we should be showing this stylesheet. + * + * @param {Document} doc + * Document for which we're checking + * @param {DOMCSSStyleSheet} sheet + * Stylesheet we're interested in + * + * @return boolean + * Whether the stylesheet should be listed. + */ + _shouldListSheet: function (doc, sheet) { + // Special case about:PreferenceStyleSheet, as it is generated on the + // fly and the URI is not registered with the about: handler. + // https://bugzilla.mozilla.org/show_bug.cgi?id=935803#c37 + if (sheet.href && sheet.href.toLowerCase() == "about:preferencestylesheet") { + return false; + } + + return true; + }, + + /** + * Add all the stylesheets for the document in this window to the map and + * create an actor for each one if not already created. + * + * @param {Window} win + * Window for which to add stylesheets + * + * @return {Promise} + * Promise that resolves to an array of StyleSheetActors + */ + _addStyleSheets: function (win) + { + return Task.spawn(function* () { + let doc = win.document; + // readyState can be uninitialized if an iframe has just been created but + // it has not started to load yet. + if (doc.readyState === "loading" || doc.readyState === "uninitialized") { + // Wait for the document to load first. + yield listenOnce(win, "DOMContentLoaded", true); + + // Make sure we have the actual document for this window. If the + // readyState was initially uninitialized, the initial dummy document + // was replaced with the actual document (bug 1171919). + doc = win.document; + } + + let isChrome = Services.scriptSecurityManager.isSystemPrincipal(doc.nodePrincipal); + let styleSheets = isChrome ? DOMUtils.getAllStyleSheets(doc) : doc.styleSheets; + let actors = []; + for (let i = 0; i < styleSheets.length; i++) { + let sheet = styleSheets[i]; + if (!this._shouldListSheet(doc, sheet)) { + continue; + } + + let actor = this.parentActor.createStyleSheetActor(sheet); + actors.push(actor); + + // Get all sheets, including imported ones + let imports = yield this._getImported(doc, actor); + actors = actors.concat(imports); + } + return actors; + }.bind(this)); + }, + + /** + * Get all the stylesheets @imported from a stylesheet. + * + * @param {Document} doc + * The document including the stylesheet + * @param {DOMStyleSheet} styleSheet + * Style sheet to search + * @return {Promise} + * A promise that resolves with an array of StyleSheetActors + */ + _getImported: function (doc, styleSheet) { + return Task.spawn(function* () { + let rules = yield styleSheet.getCSSRules(); + let imported = []; + + for (let i = 0; i < rules.length; i++) { + let rule = rules[i]; + if (rule.type == Ci.nsIDOMCSSRule.IMPORT_RULE) { + // Associated styleSheet may be null if it has already been seen due + // to duplicate @imports for the same URL. + if (!rule.styleSheet || !this._shouldListSheet(doc, rule.styleSheet)) { + continue; + } + let actor = this.parentActor.createStyleSheetActor(rule.styleSheet); + imported.push(actor); + + // recurse imports in this stylesheet as well + let children = yield this._getImported(doc, actor); + imported = imported.concat(children); + } + else if (rule.type != Ci.nsIDOMCSSRule.CHARSET_RULE) { + // @import rules must precede all others except @charset + break; + } + } + + return imported; + }.bind(this)); + }, + + + /** + * Create a new style sheet in the document with the given text. + * Return an actor for it. + * + * @param {object} request + * Debugging protocol request object, with 'text property' + * @return {object} + * Object with 'styelSheet' property for form on new actor. + */ + addStyleSheet: function (text) { + let parent = this.document.documentElement; + let style = this.document.createElementNS("http://www.w3.org/1999/xhtml", "style"); + style.setAttribute("type", "text/css"); + + if (text) { + style.appendChild(this.document.createTextNode(text)); + } + parent.appendChild(style); + + let actor = this.parentActor.createStyleSheetActor(style.sheet); + return actor; + } +}); + +exports.StyleSheetsActor = StyleSheetsActor; + +/** + * Normalize multiple relative paths towards the base paths on the right. + */ +function normalize(...aURLs) { + let base = Services.io.newURI(aURLs.pop(), null, null); + let url; + while ((url = aURLs.pop())) { + base = Services.io.newURI(url, null, base); + } + return base.spec; +} + +function dirname(aPath) { + return Services.io.newURI( + ".", null, Services.io.newURI(aPath, null, null)).spec; +} |