/* 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 {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) { 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; this._onNewStyleSheetActor = this._onNewStyleSheetActor.bind(this); this._onSheetAdded = this._onSheetAdded.bind(this); this._onWindowReady = this._onWindowReady.bind(this); events.on(this.parentActor, "stylesheet-added", this._onNewStyleSheetActor); events.on(this.parentActor, "window-ready", this._onWindowReady); // We listen for StyleSheetApplicableStateChanged rather than // StyleSheetAdded, because the latter will be sent before the // rules are ready. Using the former (with a check to ensure that // the sheet is enabled) ensures that the sheet is ready before we // try to make an actor for it. this.parentActor.chromeEventHandler .addEventListener("StyleSheetApplicableStateChanged", this._onSheetAdded, true); // This is used when creating a new style sheet, so that we can // pass the correct flag when emitting our stylesheet-added event. // See addStyleSheet and _onNewStyleSheetActor for more details. this._nextStyleSheetIsNew = false; }, destroy: function () { for (let win of this.parentActor.windows) { // This flag only exists for devtools, so we are free to clear // it when we're done. win.document.styleSheetChangeEventsEnabled = false; } events.off(this.parentActor, "stylesheet-added", this._onNewStyleSheetActor); events.off(this.parentActor, "window-ready", this._onWindowReady); this.parentActor.chromeEventHandler.removeEventListener("StyleSheetAdded", this._onSheetAdded, true); protocol.Actor.prototype.destroy.call(this); }, /** * Event handler that is called when a the tab actor emits window-ready. * * @param {Event} evt * The triggering event. */ _onWindowReady: function (evt) { this._addStyleSheets(evt.window); }, /** * Event handler that is called when a the tab actor emits stylesheet-added. * * @param {StyleSheetActor} actor * The new style sheet actor. */ _onNewStyleSheetActor: function (actor) { // Forward it to the client side. events.emit(this, "stylesheet-added", actor, this._nextStyleSheetIsNew); this._nextStyleSheetIsNew = false; }, /** * Protocol method for getting a list of StyleSheetActors representing * all the style sheets in this document. */ getStyleSheets: Task.async(function* () { let actors = []; for (let win of this.parentActor.windows) { let sheets = yield this._addStyleSheets(win); actors = actors.concat(sheets); } return actors; }), /** * Check if we should be showing this stylesheet. * * @param {DOMCSSStyleSheet} sheet * Stylesheet we're interested in * * @return boolean * Whether the stylesheet should be listed. */ _shouldListSheet: function (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; }, /** * Event handler that is called when a new style sheet is added to * a document. In particular, StyleSheetApplicableStateChanged is * listened for, because StyleSheetAdded is sent too early, before * the rules are ready. * * @param {Event} evt * The triggering event. */ _onSheetAdded: function (evt) { let sheet = evt.stylesheet; if (this._shouldListSheet(sheet)) { this.parentActor.createStyleSheetActor(sheet); } }, /** * 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; // We have to set this flag in order to get the // StyleSheetApplicableStateChanged events. See Document.webidl. doc.styleSheetChangeEventsEnabled = true; 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(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(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) { // This is a bit convoluted. The style sheet actor may be created // by a notification from platform. In this case, we can't easily // pass the "new" flag through to createStyleSheetActor, so we set // a flag locally and check it before sending an event to the // client. See |_onNewStyleSheetActor|. this._nextStyleSheetIsNew = true; 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; }