123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890 |
- /* vim:set ts=2 sw=2 sts=2 et: */
- /* 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";
- this.EXPORTED_SYMBOLS = ["StyleSheetEditor"];
- const Cc = Components.classes;
- const Ci = Components.interfaces;
- const Cu = Components.utils;
- const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
- const Editor = require("devtools/client/sourceeditor/editor");
- const promise = require("promise");
- const defer = require("devtools/shared/defer");
- const {shortSource, prettifyCSS} = require("devtools/shared/inspector/css-logic");
- const {console} = require("resource://gre/modules/Console.jsm");
- const Services = require("Services");
- const EventEmitter = require("devtools/shared/event-emitter");
- const {Task} = require("devtools/shared/task");
- const {FileUtils} = require("resource://gre/modules/FileUtils.jsm");
- const {NetUtil} = require("resource://gre/modules/NetUtil.jsm");
- const {TextDecoder, OS} = Cu.import("resource://gre/modules/osfile.jsm", {});
- const {
- getString,
- showFilePicker,
- } = require("resource://devtools/client/styleeditor/StyleEditorUtil.jsm");
- const LOAD_ERROR = "error-load";
- const SAVE_ERROR = "error-save";
- // max update frequency in ms (avoid potential typing lag and/or flicker)
- // @see StyleEditor.updateStylesheet
- const UPDATE_STYLESHEET_DELAY = 500;
- // Pref which decides if CSS autocompletion is enabled in Style Editor or not.
- const AUTOCOMPLETION_PREF = "devtools.styleeditor.autocompletion-enabled";
- // Pref which decides whether updates to the stylesheet use transitions
- const TRANSITION_PREF = "devtools.styleeditor.transitions";
- // How long to wait to update linked CSS file after original source was saved
- // to disk. Time in ms.
- const CHECK_LINKED_SHEET_DELAY = 500;
- // How many times to check for linked file changes
- const MAX_CHECK_COUNT = 10;
- // The classname used to show a line that is not used
- const UNUSED_CLASS = "cm-unused-line";
- // How much time should the mouse be still before the selector at that position
- // gets highlighted?
- const SELECTOR_HIGHLIGHT_TIMEOUT = 500;
- /**
- * StyleSheetEditor controls the editor linked to a particular StyleSheet
- * object.
- *
- * Emits events:
- * 'property-change': A property on the underlying stylesheet has changed
- * 'source-editor-load': The source editor for this editor has been loaded
- * 'error': An error has occured
- *
- * @param {StyleSheet|OriginalSource} styleSheet
- * Stylesheet or original source to show
- * @param {DOMWindow} win
- * panel window for style editor
- * @param {nsIFile} file
- * Optional file that the sheet was imported from
- * @param {boolean} isNew
- * Optional whether the sheet was created by the user
- * @param {Walker} walker
- * Optional walker used for selectors autocompletion
- * @param {CustomHighlighterFront} highlighter
- * Optional highlighter front for the SelectorHighligher used to
- * highlight selectors
- */
- function StyleSheetEditor(styleSheet, win, file, isNew, walker, highlighter) {
- EventEmitter.decorate(this);
- this.styleSheet = styleSheet;
- this._inputElement = null;
- this.sourceEditor = null;
- this._window = win;
- this._isNew = isNew;
- this.walker = walker;
- this.highlighter = highlighter;
- // True when we've called update() on the style sheet.
- this._isUpdating = false;
- // True when we've just set the editor text based on a style-applied
- // event from the StyleSheetActor.
- this._justSetText = false;
- // state to use when inputElement attaches
- this._state = {
- text: "",
- selection: {
- start: {line: 0, ch: 0},
- end: {line: 0, ch: 0}
- }
- };
- this._styleSheetFilePath = null;
- if (styleSheet.href &&
- Services.io.extractScheme(this.styleSheet.href) == "file") {
- this._styleSheetFilePath = this.styleSheet.href;
- }
- this._onPropertyChange = this._onPropertyChange.bind(this);
- this._onError = this._onError.bind(this);
- this._onMediaRuleMatchesChange = this._onMediaRuleMatchesChange.bind(this);
- this._onMediaRulesChanged = this._onMediaRulesChanged.bind(this);
- this._onStyleApplied = this._onStyleApplied.bind(this);
- this.checkLinkedFileForChanges = this.checkLinkedFileForChanges.bind(this);
- this.markLinkedFileBroken = this.markLinkedFileBroken.bind(this);
- this.saveToFile = this.saveToFile.bind(this);
- this.updateStyleSheet = this.updateStyleSheet.bind(this);
- this._updateStyleSheet = this._updateStyleSheet.bind(this);
- this._onMouseMove = this._onMouseMove.bind(this);
- this._focusOnSourceEditorReady = false;
- this.cssSheet.on("property-change", this._onPropertyChange);
- this.styleSheet.on("error", this._onError);
- this.mediaRules = [];
- if (this.cssSheet.getMediaRules) {
- this.cssSheet.getMediaRules().then(this._onMediaRulesChanged,
- e => console.error(e));
- }
- this.cssSheet.on("media-rules-changed", this._onMediaRulesChanged);
- this.cssSheet.on("style-applied", this._onStyleApplied);
- this.savedFile = file;
- this.linkCSSFile();
- }
- this.StyleSheetEditor = StyleSheetEditor;
- StyleSheetEditor.prototype = {
- /**
- * Whether there are unsaved changes in the editor
- */
- get unsaved() {
- return this.sourceEditor && !this.sourceEditor.isClean();
- },
- /**
- * Whether the editor is for a stylesheet created by the user
- * through the style editor UI.
- */
- get isNew() {
- return this._isNew;
- },
- /**
- * The style sheet or the generated style sheet for this source if it's an
- * original source.
- */
- get cssSheet() {
- if (this.styleSheet.isOriginalSource) {
- return this.styleSheet.relatedStyleSheet;
- }
- return this.styleSheet;
- },
- get savedFile() {
- return this._savedFile;
- },
- set savedFile(name) {
- this._savedFile = name;
- this.linkCSSFile();
- },
- /**
- * Get a user-friendly name for the style sheet.
- *
- * @return string
- */
- get friendlyName() {
- if (this.savedFile) {
- return this.savedFile.leafName;
- }
- if (this._isNew) {
- let index = this.styleSheet.styleSheetIndex + 1;
- return getString("newStyleSheet", index);
- }
- if (!this.styleSheet.href) {
- let index = this.styleSheet.styleSheetIndex + 1;
- return getString("inlineStyleSheet", index);
- }
- if (!this._friendlyName) {
- let sheetURI = this.styleSheet.href;
- this._friendlyName = shortSource({ href: sheetURI });
- try {
- this._friendlyName = decodeURI(this._friendlyName);
- } catch (ex) {
- // Ignore.
- }
- }
- return this._friendlyName;
- },
- /**
- * Check if transitions are enabled for style changes.
- *
- * @return Boolean
- */
- get transitionsEnabled() {
- return Services.prefs.getBoolPref(TRANSITION_PREF);
- },
- /**
- * If this is an original source, get the path of the CSS file it generated.
- */
- linkCSSFile: function () {
- if (!this.styleSheet.isOriginalSource) {
- return;
- }
- let relatedSheet = this.styleSheet.relatedStyleSheet;
- if (!relatedSheet || !relatedSheet.href) {
- return;
- }
- let path;
- let href = removeQuery(relatedSheet.href);
- let uri = NetUtil.newURI(href);
- if (uri.scheme == "file") {
- let file = uri.QueryInterface(Ci.nsIFileURL).file;
- path = file.path;
- } else if (this.savedFile) {
- let origHref = removeQuery(this.styleSheet.href);
- let origUri = NetUtil.newURI(origHref);
- path = findLinkedFilePath(uri, origUri, this.savedFile);
- } else {
- // we can't determine path to generated file on disk
- return;
- }
- if (this.linkedCSSFile == path) {
- return;
- }
- this.linkedCSSFile = path;
- this.linkedCSSFileError = null;
- // save last file change time so we can compare when we check for changes.
- OS.File.stat(path).then((info) => {
- this._fileModDate = info.lastModificationDate.getTime();
- }, this.markLinkedFileBroken);
- this.emit("linked-css-file");
- },
- /**
- * A helper function that fetches the source text from the style
- * sheet. The text is possibly prettified using prettifyCSS. This
- * also sets |this._state.text| to the new text.
- *
- * @return {Promise} a promise that resolves to the new text
- */
- _getSourceTextAndPrettify: function () {
- return this.styleSheet.getText().then((longStr) => {
- return longStr.string();
- }).then((source) => {
- let ruleCount = this.styleSheet.ruleCount;
- if (!this.styleSheet.isOriginalSource) {
- source = prettifyCSS(source, ruleCount);
- }
- this._state.text = source;
- return source;
- });
- },
- /**
- * Start fetching the full text source for this editor's sheet.
- *
- * @return {Promise}
- * A promise that'll resolve with the source text once the source
- * has been loaded or reject on unexpected error.
- */
- fetchSource: function () {
- return this._getSourceTextAndPrettify().then((source) => {
- this.sourceLoaded = true;
- return source;
- }).then(null, e => {
- if (this._isDestroyed) {
- console.warn("Could not fetch the source for " +
- this.styleSheet.href +
- ", the editor was destroyed");
- console.error(e);
- } else {
- this.emit("error", { key: LOAD_ERROR, append: this.styleSheet.href });
- throw e;
- }
- });
- },
- /**
- * Add markup to a region. UNUSED_CLASS is added to specified lines
- * @param region An object shaped like
- * {
- * start: { line: L1, column: C1 },
- * end: { line: L2, column: C2 } // optional
- * }
- */
- addUnusedRegion: function (region) {
- this.sourceEditor.addLineClass(region.start.line - 1, UNUSED_CLASS);
- if (region.end) {
- for (let i = region.start.line; i <= region.end.line; i++) {
- this.sourceEditor.addLineClass(i - 1, UNUSED_CLASS);
- }
- }
- },
- /**
- * As addUnusedRegion except that it takes an array of regions
- */
- addUnusedRegions: function (regions) {
- for (let region of regions) {
- this.addUnusedRegion(region);
- }
- },
- /**
- * Remove all the unused markup regions added by addUnusedRegion
- */
- removeAllUnusedRegions: function () {
- for (let i = 0; i < this.sourceEditor.lineCount(); i++) {
- this.sourceEditor.removeLineClass(i, UNUSED_CLASS);
- }
- },
- /**
- * Forward property-change event from stylesheet.
- *
- * @param {string} event
- * Event type
- * @param {string} property
- * Property that has changed on sheet
- */
- _onPropertyChange: function (property, value) {
- this.emit("property-change", property, value);
- },
- /**
- * Called when the stylesheet text changes.
- */
- _onStyleApplied: function () {
- if (this._isUpdating) {
- // We just applied an edit in the editor, so we can drop this
- // notification.
- this._isUpdating = false;
- } else if (this.sourceEditor) {
- this._getSourceTextAndPrettify().then((newText) => {
- this._justSetText = true;
- let firstLine = this.sourceEditor.getFirstVisibleLine();
- let pos = this.sourceEditor.getCursor();
- this.sourceEditor.setText(newText);
- this.sourceEditor.setFirstVisibleLine(firstLine);
- this.sourceEditor.setCursor(pos);
- this.emit("style-applied");
- });
- }
- },
- /**
- * Handles changes to the list of @media rules in the stylesheet.
- * Emits 'media-rules-changed' if the list has changed.
- *
- * @param {array} rules
- * Array of MediaRuleFronts for new media rules of sheet.
- */
- _onMediaRulesChanged: function (rules) {
- if (!rules.length && !this.mediaRules.length) {
- return;
- }
- for (let rule of this.mediaRules) {
- rule.off("matches-change", this._onMediaRuleMatchesChange);
- rule.destroy();
- }
- this.mediaRules = rules;
- for (let rule of rules) {
- rule.on("matches-change", this._onMediaRuleMatchesChange);
- }
- this.emit("media-rules-changed", rules);
- },
- /**
- * Forward media-rules-changed event from stylesheet.
- */
- _onMediaRuleMatchesChange: function () {
- this.emit("media-rules-changed", this.mediaRules);
- },
- /**
- * Forward error event from stylesheet.
- *
- * @param {string} event
- * Event type
- * @param {string} errorCode
- */
- _onError: function (event, data) {
- this.emit("error", data);
- },
- /**
- * Create source editor and load state into it.
- * @param {DOMElement} inputElement
- * Element to load source editor in
- * @param {CssProperties} cssProperties
- * A css properties database.
- *
- * @return {Promise}
- * Promise that will resolve when the style editor is loaded.
- */
- load: function (inputElement, cssProperties) {
- if (this._isDestroyed) {
- return promise.reject("Won't load source editor as the style sheet has " +
- "already been removed from Style Editor.");
- }
- this._inputElement = inputElement;
- let config = {
- value: this._state.text,
- lineNumbers: true,
- mode: Editor.modes.css,
- readOnly: false,
- autoCloseBrackets: "{}()",
- extraKeys: this._getKeyBindings(),
- contextMenu: "sourceEditorContextMenu",
- autocomplete: Services.prefs.getBoolPref(AUTOCOMPLETION_PREF),
- autocompleteOpts: { walker: this.walker, cssProperties },
- cssProperties
- };
- let sourceEditor = this._sourceEditor = new Editor(config);
- sourceEditor.on("dirty-change", this._onPropertyChange);
- return sourceEditor.appendTo(inputElement).then(() => {
- sourceEditor.on("saveRequested", this.saveToFile);
- if (this.styleSheet.update) {
- sourceEditor.on("change", this.updateStyleSheet);
- }
- this.sourceEditor = sourceEditor;
- if (this._focusOnSourceEditorReady) {
- this._focusOnSourceEditorReady = false;
- sourceEditor.focus();
- }
- sourceEditor.setSelection(this._state.selection.start,
- this._state.selection.end);
- if (this.highlighter && this.walker) {
- sourceEditor.container.addEventListener("mousemove", this._onMouseMove);
- }
- // Add the commands controller for the source-editor.
- sourceEditor.insertCommandsController();
- this.emit("source-editor-load");
- });
- },
- /**
- * Get the source editor for this editor.
- *
- * @return {Promise}
- * Promise that will resolve with the editor.
- */
- getSourceEditor: function () {
- let deferred = defer();
- if (this.sourceEditor) {
- return promise.resolve(this);
- }
- this.on("source-editor-load", () => {
- deferred.resolve(this);
- });
- return deferred.promise;
- },
- /**
- * Focus the Style Editor input.
- */
- focus: function () {
- if (this.sourceEditor) {
- this.sourceEditor.focus();
- } else {
- this._focusOnSourceEditorReady = true;
- }
- },
- /**
- * Event handler for when the editor is shown.
- */
- onShow: function () {
- if (this.sourceEditor) {
- // CodeMirror needs refresh to restore scroll position after hiding and
- // showing the editor.
- this.sourceEditor.refresh();
- }
- this.focus();
- },
- /**
- * Toggled the disabled state of the underlying stylesheet.
- */
- toggleDisabled: function () {
- this.styleSheet.toggleDisabled().then(null, e => console.error(e));
- },
- /**
- * Queue a throttled task to update the live style sheet.
- */
- updateStyleSheet: function () {
- if (this._updateTask) {
- // cancel previous queued task not executed within throttle delay
- this._window.clearTimeout(this._updateTask);
- }
- this._updateTask = this._window.setTimeout(this._updateStyleSheet,
- UPDATE_STYLESHEET_DELAY);
- },
- /**
- * Update live style sheet according to modifications.
- */
- _updateStyleSheet: function () {
- if (this.styleSheet.disabled) {
- // TODO: do we want to do this?
- return;
- }
- if (this._justSetText) {
- this._justSetText = false;
- return;
- }
- // reset only if we actually perform an update
- // (stylesheet is enabled) so that 'missed' updates
- // while the stylesheet is disabled can be performed
- // when it is enabled back. @see enableStylesheet
- this._updateTask = null;
- if (this.sourceEditor) {
- this._state.text = this.sourceEditor.getText();
- }
- this._isUpdating = true;
- this.styleSheet.update(this._state.text, this.transitionsEnabled)
- .then(null, e => console.error(e));
- },
- /**
- * Handle mousemove events, calling _highlightSelectorAt after a delay only
- * and reseting the delay everytime.
- */
- _onMouseMove: function (e) {
- this.highlighter.hide();
- if (this.mouseMoveTimeout) {
- this._window.clearTimeout(this.mouseMoveTimeout);
- this.mouseMoveTimeout = null;
- }
- this.mouseMoveTimeout = this._window.setTimeout(() => {
- this._highlightSelectorAt(e.clientX, e.clientY);
- }, SELECTOR_HIGHLIGHT_TIMEOUT);
- },
- /**
- * Highlight nodes matching the selector found at coordinates x,y in the
- * editor, if any.
- *
- * @param {Number} x
- * @param {Number} y
- */
- _highlightSelectorAt: Task.async(function* (x, y) {
- let pos = this.sourceEditor.getPositionFromCoords({left: x, top: y});
- let info = this.sourceEditor.getInfoAt(pos);
- if (!info || info.state !== "selector") {
- return;
- }
- let node =
- yield this.walker.getStyleSheetOwnerNode(this.styleSheet.actorID);
- yield this.highlighter.show(node, {
- selector: info.selector,
- hideInfoBar: true,
- showOnly: "border",
- region: "border"
- });
- this.emit("node-highlighted");
- }),
- /**
- * Save the editor contents into a file and set savedFile property.
- * A file picker UI will open if file is not set and editor is not headless.
- *
- * @param mixed file
- * Optional nsIFile or string representing the filename to save in the
- * background, no UI will be displayed.
- * If not specified, the original style sheet URI is used.
- * To implement 'Save' instead of 'Save as', you can pass
- * savedFile here.
- * @param function(nsIFile aFile) callback
- * Optional callback called when the operation has finished.
- * aFile has the nsIFile object for saved file or null if the operation
- * has failed or has been canceled by the user.
- * @see savedFile
- */
- saveToFile: function (file, callback) {
- let onFile = (returnFile) => {
- if (!returnFile) {
- if (callback) {
- callback(null);
- }
- return;
- }
- if (this.sourceEditor) {
- this._state.text = this.sourceEditor.getText();
- }
- let ostream = FileUtils.openSafeFileOutputStream(returnFile);
- let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
- .createInstance(Ci.nsIScriptableUnicodeConverter);
- converter.charset = "UTF-8";
- let istream = converter.convertToInputStream(this._state.text);
- NetUtil.asyncCopy(istream, ostream, (status) => {
- if (!Components.isSuccessCode(status)) {
- if (callback) {
- callback(null);
- }
- this.emit("error", { key: SAVE_ERROR });
- return;
- }
- FileUtils.closeSafeFileOutputStream(ostream);
- this.onFileSaved(returnFile);
- if (callback) {
- callback(returnFile);
- }
- });
- };
- let defaultName;
- if (this._friendlyName) {
- defaultName = OS.Path.basename(this._friendlyName);
- }
- showFilePicker(file || this._styleSheetFilePath, true, this._window,
- onFile, defaultName);
- },
- /**
- * Called when this source has been successfully saved to disk.
- */
- onFileSaved: function (returnFile) {
- this._friendlyName = null;
- this.savedFile = returnFile;
- if (this.sourceEditor) {
- this.sourceEditor.setClean();
- }
- this.emit("property-change");
- // TODO: replace with file watching
- this._modCheckCount = 0;
- this._window.clearTimeout(this._timeout);
- if (this.linkedCSSFile && !this.linkedCSSFileError) {
- this._timeout = this._window.setTimeout(this.checkLinkedFileForChanges,
- CHECK_LINKED_SHEET_DELAY);
- }
- },
- /**
- * Check to see if our linked CSS file has changed on disk, and
- * if so, update the live style sheet.
- */
- checkLinkedFileForChanges: function () {
- OS.File.stat(this.linkedCSSFile).then((info) => {
- let lastChange = info.lastModificationDate.getTime();
- if (this._fileModDate && lastChange != this._fileModDate) {
- this._fileModDate = lastChange;
- this._modCheckCount = 0;
- this.updateLinkedStyleSheet();
- return;
- }
- if (++this._modCheckCount > MAX_CHECK_COUNT) {
- this.updateLinkedStyleSheet();
- return;
- }
- // try again in a bit
- this._timeout = this._window.setTimeout(this.checkLinkedFileForChanges,
- CHECK_LINKED_SHEET_DELAY);
- }, this.markLinkedFileBroken);
- },
- /**
- * Notify that the linked CSS file (if this is an original source)
- * doesn't exist on disk in the place we think it does.
- *
- * @param string error
- * The error we got when trying to access the file.
- */
- markLinkedFileBroken: function (error) {
- this.linkedCSSFileError = error || true;
- this.emit("linked-css-file-error");
- error += " querying " + this.linkedCSSFile +
- " original source location: " + this.savedFile.path;
- console.error(error);
- },
- /**
- * For original sources (e.g. Sass files). Fetch contents of linked CSS
- * file from disk and live update the stylesheet object with the contents.
- */
- updateLinkedStyleSheet: function () {
- OS.File.read(this.linkedCSSFile).then((array) => {
- let decoder = new TextDecoder();
- let text = decoder.decode(array);
- let relatedSheet = this.styleSheet.relatedStyleSheet;
- relatedSheet.update(text, this.transitionsEnabled);
- }, this.markLinkedFileBroken);
- },
- /**
- * Retrieve custom key bindings objects as expected by Editor.
- * Editor action names are not displayed to the user.
- *
- * @return {array} key binding objects for the source editor
- */
- _getKeyBindings: function () {
- let bindings = {};
- let keybind = Editor.accel(getString("saveStyleSheet.commandkey"));
- bindings[keybind] = () => {
- this.saveToFile(this.savedFile);
- };
- bindings["Shift-" + keybind] = () => {
- this.saveToFile();
- };
- bindings.Esc = false;
- return bindings;
- },
- /**
- * Clean up for this editor.
- */
- destroy: function () {
- if (this._sourceEditor) {
- this._sourceEditor.off("dirty-change", this._onPropertyChange);
- this._sourceEditor.off("saveRequested", this.saveToFile);
- this._sourceEditor.off("change", this.updateStyleSheet);
- if (this.highlighter && this.walker && this._sourceEditor.container) {
- this._sourceEditor.container.removeEventListener("mousemove",
- this._onMouseMove);
- }
- this._sourceEditor.destroy();
- }
- this.cssSheet.off("property-change", this._onPropertyChange);
- this.cssSheet.off("media-rules-changed", this._onMediaRulesChanged);
- this.cssSheet.off("style-applied", this._onStyleApplied);
- this.styleSheet.off("error", this._onError);
- this._isDestroyed = true;
- }
- };
- /**
- * Find a path on disk for a file given it's hosted uri, the uri of the
- * original resource that generated it (e.g. Sass file), and the location of the
- * local file for that source.
- *
- * @param {nsIURI} uri
- * The uri of the resource
- * @param {nsIURI} origUri
- * The uri of the original source for the resource
- * @param {nsIFile} file
- * The local file for the resource on disk
- *
- * @return {string}
- * The path of original file on disk
- */
- function findLinkedFilePath(uri, origUri, file) {
- let { origBranch, branch } = findUnsharedBranches(origUri, uri);
- let project = findProjectPath(file, origBranch);
- let parts = project.concat(branch);
- let path = OS.Path.join.apply(this, parts);
- return path;
- }
- /**
- * Find the path of a project given a file in the project and its branch
- * off the root. e.g.:
- * /Users/moz/proj/src/a.css" and "src/a.css"
- * would yield ["Users", "moz", "proj"]
- *
- * @param {nsIFile} file
- * file for that resource on disk
- * @param {array} branch
- * path parts for branch to chop off file path.
- * @return {array}
- * array of path parts
- */
- function findProjectPath(file, branch) {
- let path = OS.Path.split(file.path).components;
- for (let i = 2; i <= branch.length; i++) {
- // work backwards until we find a differing directory name
- if (path[path.length - i] != branch[branch.length - i]) {
- return path.slice(0, path.length - i + 1);
- }
- }
- // if we don't find a differing directory, just chop off the branch
- return path.slice(0, path.length - branch.length);
- }
- /**
- * Find the parts of a uri past the root it shares with another uri. e.g:
- * "http://localhost/built/a.scss" and "http://localhost/src/a.css"
- * would yield ["built", "a.scss"] and ["src", "a.css"]
- *
- * @param {nsIURI} origUri
- * uri to find unshared branch of. Usually is uri for original source.
- * @param {nsIURI} uri
- * uri to compare against to get a shared root
- * @return {object}
- * object with 'branch' and 'origBranch' array of path parts for branch
- */
- function findUnsharedBranches(origUri, uri) {
- origUri = OS.Path.split(origUri.path).components;
- uri = OS.Path.split(uri.path).components;
- for (let i = 0; i < uri.length - 1; i++) {
- if (uri[i] != origUri[i]) {
- return {
- branch: uri.slice(i),
- origBranch: origUri.slice(i)
- };
- }
- }
- return {
- branch: uri,
- origBranch: origUri
- };
- }
- /**
- * Remove the query string from a url.
- *
- * @param {string} href
- * Url to remove query string from
- * @return {string}
- * Url without query string
- */
- function removeQuery(href) {
- return href.replace(/\?.*/, "");
- }
|