1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/devtools/styleeditor/StyleSheetEditor.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,723 @@ 1.4 +/* vim:set ts=2 sw=2 sts=2 et: */ 1.5 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +"use strict"; 1.10 + 1.11 +this.EXPORTED_SYMBOLS = ["StyleSheetEditor"]; 1.12 + 1.13 +const Cc = Components.classes; 1.14 +const Ci = Components.interfaces; 1.15 +const Cu = Components.utils; 1.16 + 1.17 +const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require; 1.18 +const Editor = require("devtools/sourceeditor/editor"); 1.19 +const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); 1.20 +const {CssLogic} = require("devtools/styleinspector/css-logic"); 1.21 +const AutoCompleter = require("devtools/sourceeditor/autocomplete"); 1.22 + 1.23 +Cu.import("resource://gre/modules/Services.jsm"); 1.24 +Cu.import("resource://gre/modules/FileUtils.jsm"); 1.25 +Cu.import("resource://gre/modules/NetUtil.jsm"); 1.26 +Cu.import("resource://gre/modules/osfile.jsm"); 1.27 +Cu.import("resource://gre/modules/devtools/event-emitter.js"); 1.28 +Cu.import("resource:///modules/devtools/StyleEditorUtil.jsm"); 1.29 + 1.30 +const LOAD_ERROR = "error-load"; 1.31 +const SAVE_ERROR = "error-save"; 1.32 + 1.33 +// max update frequency in ms (avoid potential typing lag and/or flicker) 1.34 +// @see StyleEditor.updateStylesheet 1.35 +const UPDATE_STYLESHEET_THROTTLE_DELAY = 500; 1.36 + 1.37 +// Pref which decides if CSS autocompletion is enabled in Style Editor or not. 1.38 +const AUTOCOMPLETION_PREF = "devtools.styleeditor.autocompletion-enabled"; 1.39 + 1.40 +// How long to wait to update linked CSS file after original source was saved 1.41 +// to disk. Time in ms. 1.42 +const CHECK_LINKED_SHEET_DELAY=500; 1.43 + 1.44 +// How many times to check for linked file changes 1.45 +const MAX_CHECK_COUNT=10; 1.46 + 1.47 +/** 1.48 + * StyleSheetEditor controls the editor linked to a particular StyleSheet 1.49 + * object. 1.50 + * 1.51 + * Emits events: 1.52 + * 'property-change': A property on the underlying stylesheet has changed 1.53 + * 'source-editor-load': The source editor for this editor has been loaded 1.54 + * 'error': An error has occured 1.55 + * 1.56 + * @param {StyleSheet|OriginalSource} styleSheet 1.57 + * Stylesheet or original source to show 1.58 + * @param {DOMWindow} win 1.59 + * panel window for style editor 1.60 + * @param {nsIFile} file 1.61 + * Optional file that the sheet was imported from 1.62 + * @param {boolean} isNew 1.63 + * Optional whether the sheet was created by the user 1.64 + * @param {Walker} walker 1.65 + * Optional walker used for selectors autocompletion 1.66 + */ 1.67 +function StyleSheetEditor(styleSheet, win, file, isNew, walker) { 1.68 + EventEmitter.decorate(this); 1.69 + 1.70 + this.styleSheet = styleSheet; 1.71 + this._inputElement = null; 1.72 + this.sourceEditor = null; 1.73 + this._window = win; 1.74 + this._isNew = isNew; 1.75 + this.walker = walker; 1.76 + 1.77 + this._state = { // state to use when inputElement attaches 1.78 + text: "", 1.79 + selection: { 1.80 + start: {line: 0, ch: 0}, 1.81 + end: {line: 0, ch: 0} 1.82 + }, 1.83 + topIndex: 0 // the first visible line 1.84 + }; 1.85 + 1.86 + this._styleSheetFilePath = null; 1.87 + if (styleSheet.href && 1.88 + Services.io.extractScheme(this.styleSheet.href) == "file") { 1.89 + this._styleSheetFilePath = this.styleSheet.href; 1.90 + } 1.91 + 1.92 + this._onPropertyChange = this._onPropertyChange.bind(this); 1.93 + this._onError = this._onError.bind(this); 1.94 + this.checkLinkedFileForChanges = this.checkLinkedFileForChanges.bind(this); 1.95 + this.markLinkedFileBroken = this.markLinkedFileBroken.bind(this); 1.96 + 1.97 + this._focusOnSourceEditorReady = false; 1.98 + 1.99 + let relatedSheet = this.styleSheet.relatedStyleSheet; 1.100 + if (relatedSheet) { 1.101 + relatedSheet.on("property-change", this._onPropertyChange); 1.102 + } 1.103 + this.styleSheet.on("property-change", this._onPropertyChange); 1.104 + this.styleSheet.on("error", this._onError); 1.105 + 1.106 + this.savedFile = file; 1.107 + this.linkCSSFile(); 1.108 +} 1.109 + 1.110 +StyleSheetEditor.prototype = { 1.111 + /** 1.112 + * Whether there are unsaved changes in the editor 1.113 + */ 1.114 + get unsaved() { 1.115 + return this.sourceEditor && !this.sourceEditor.isClean(); 1.116 + }, 1.117 + 1.118 + /** 1.119 + * Whether the editor is for a stylesheet created by the user 1.120 + * through the style editor UI. 1.121 + */ 1.122 + get isNew() { 1.123 + return this._isNew; 1.124 + }, 1.125 + 1.126 + get savedFile() { 1.127 + return this._savedFile; 1.128 + }, 1.129 + 1.130 + set savedFile(name) { 1.131 + this._savedFile = name; 1.132 + 1.133 + this.linkCSSFile(); 1.134 + }, 1.135 + 1.136 + /** 1.137 + * Get a user-friendly name for the style sheet. 1.138 + * 1.139 + * @return string 1.140 + */ 1.141 + get friendlyName() { 1.142 + if (this.savedFile) { 1.143 + return this.savedFile.leafName; 1.144 + } 1.145 + 1.146 + if (this._isNew) { 1.147 + let index = this.styleSheet.styleSheetIndex + 1; 1.148 + return _("newStyleSheet", index); 1.149 + } 1.150 + 1.151 + if (!this.styleSheet.href) { 1.152 + let index = this.styleSheet.styleSheetIndex + 1; 1.153 + return _("inlineStyleSheet", index); 1.154 + } 1.155 + 1.156 + if (!this._friendlyName) { 1.157 + let sheetURI = this.styleSheet.href; 1.158 + this._friendlyName = CssLogic.shortSource({ href: sheetURI }); 1.159 + try { 1.160 + this._friendlyName = decodeURI(this._friendlyName); 1.161 + } catch (ex) { 1.162 + } 1.163 + } 1.164 + return this._friendlyName; 1.165 + }, 1.166 + 1.167 + /** 1.168 + * If this is an original source, get the path of the CSS file it generated. 1.169 + */ 1.170 + linkCSSFile: function() { 1.171 + if (!this.styleSheet.isOriginalSource) { 1.172 + return; 1.173 + } 1.174 + 1.175 + let relatedSheet = this.styleSheet.relatedStyleSheet; 1.176 + 1.177 + let path; 1.178 + let href = removeQuery(relatedSheet.href); 1.179 + let uri = NetUtil.newURI(href); 1.180 + 1.181 + if (uri.scheme == "file") { 1.182 + let file = uri.QueryInterface(Ci.nsIFileURL).file; 1.183 + path = file.path; 1.184 + } 1.185 + else if (this.savedFile) { 1.186 + let origHref = removeQuery(this.styleSheet.href); 1.187 + let origUri = NetUtil.newURI(origHref); 1.188 + path = findLinkedFilePath(uri, origUri, this.savedFile); 1.189 + } 1.190 + else { 1.191 + // we can't determine path to generated file on disk 1.192 + return; 1.193 + } 1.194 + 1.195 + if (this.linkedCSSFile == path) { 1.196 + return; 1.197 + } 1.198 + 1.199 + this.linkedCSSFile = path; 1.200 + 1.201 + this.linkedCSSFileError = null; 1.202 + 1.203 + // save last file change time so we can compare when we check for changes. 1.204 + OS.File.stat(path).then((info) => { 1.205 + this._fileModDate = info.lastModificationDate.getTime(); 1.206 + }, this.markLinkedFileBroken); 1.207 + 1.208 + this.emit("linked-css-file"); 1.209 + }, 1.210 + 1.211 + /** 1.212 + * Start fetching the full text source for this editor's sheet. 1.213 + */ 1.214 + fetchSource: function(callback) { 1.215 + this.styleSheet.getText().then((longStr) => { 1.216 + longStr.string().then((source) => { 1.217 + let ruleCount = this.styleSheet.ruleCount; 1.218 + this._state.text = prettifyCSS(source, ruleCount); 1.219 + this.sourceLoaded = true; 1.220 + 1.221 + callback(source); 1.222 + }); 1.223 + }, e => { 1.224 + this.emit("error", LOAD_ERROR, this.styleSheet.href); 1.225 + }) 1.226 + }, 1.227 + 1.228 + /** 1.229 + * Forward property-change event from stylesheet. 1.230 + * 1.231 + * @param {string} event 1.232 + * Event type 1.233 + * @param {string} property 1.234 + * Property that has changed on sheet 1.235 + */ 1.236 + _onPropertyChange: function(property, value) { 1.237 + this.emit("property-change", property, value); 1.238 + }, 1.239 + 1.240 + /** 1.241 + * Forward error event from stylesheet. 1.242 + * 1.243 + * @param {string} event 1.244 + * Event type 1.245 + * @param {string} errorCode 1.246 + */ 1.247 + _onError: function(event, errorCode) { 1.248 + this.emit("error", errorCode); 1.249 + }, 1.250 + 1.251 + /** 1.252 + * Create source editor and load state into it. 1.253 + * @param {DOMElement} inputElement 1.254 + * Element to load source editor in 1.255 + * 1.256 + * @return {Promise} 1.257 + * Promise that will resolve when the style editor is loaded. 1.258 + */ 1.259 + load: function(inputElement) { 1.260 + this._inputElement = inputElement; 1.261 + 1.262 + let config = { 1.263 + value: this._state.text, 1.264 + lineNumbers: true, 1.265 + mode: Editor.modes.css, 1.266 + readOnly: false, 1.267 + autoCloseBrackets: "{}()[]", 1.268 + extraKeys: this._getKeyBindings(), 1.269 + contextMenu: "sourceEditorContextMenu" 1.270 + }; 1.271 + let sourceEditor = new Editor(config); 1.272 + 1.273 + sourceEditor.on("dirty-change", this._onPropertyChange); 1.274 + 1.275 + return sourceEditor.appendTo(inputElement).then(() => { 1.276 + if (Services.prefs.getBoolPref(AUTOCOMPLETION_PREF)) { 1.277 + sourceEditor.extend(AutoCompleter); 1.278 + sourceEditor.setupAutoCompletion(this.walker); 1.279 + } 1.280 + sourceEditor.on("save", () => { 1.281 + this.saveToFile(); 1.282 + }); 1.283 + 1.284 + if (this.styleSheet.update) { 1.285 + sourceEditor.on("change", () => { 1.286 + this.updateStyleSheet(); 1.287 + }); 1.288 + } 1.289 + 1.290 + this.sourceEditor = sourceEditor; 1.291 + 1.292 + if (this._focusOnSourceEditorReady) { 1.293 + this._focusOnSourceEditorReady = false; 1.294 + sourceEditor.focus(); 1.295 + } 1.296 + 1.297 + sourceEditor.setFirstVisibleLine(this._state.topIndex); 1.298 + sourceEditor.setSelection(this._state.selection.start, 1.299 + this._state.selection.end); 1.300 + 1.301 + this.emit("source-editor-load"); 1.302 + }); 1.303 + }, 1.304 + 1.305 + /** 1.306 + * Get the source editor for this editor. 1.307 + * 1.308 + * @return {Promise} 1.309 + * Promise that will resolve with the editor. 1.310 + */ 1.311 + getSourceEditor: function() { 1.312 + let deferred = promise.defer(); 1.313 + 1.314 + if (this.sourceEditor) { 1.315 + return promise.resolve(this); 1.316 + } 1.317 + this.on("source-editor-load", () => { 1.318 + deferred.resolve(this); 1.319 + }); 1.320 + return deferred.promise; 1.321 + }, 1.322 + 1.323 + /** 1.324 + * Focus the Style Editor input. 1.325 + */ 1.326 + focus: function() { 1.327 + if (this.sourceEditor) { 1.328 + this.sourceEditor.focus(); 1.329 + } else { 1.330 + this._focusOnSourceEditorReady = true; 1.331 + } 1.332 + }, 1.333 + 1.334 + /** 1.335 + * Event handler for when the editor is shown. 1.336 + */ 1.337 + onShow: function() { 1.338 + if (this.sourceEditor) { 1.339 + this.sourceEditor.setFirstVisibleLine(this._state.topIndex); 1.340 + } 1.341 + this.focus(); 1.342 + }, 1.343 + 1.344 + /** 1.345 + * Toggled the disabled state of the underlying stylesheet. 1.346 + */ 1.347 + toggleDisabled: function() { 1.348 + this.styleSheet.toggleDisabled(); 1.349 + }, 1.350 + 1.351 + /** 1.352 + * Queue a throttled task to update the live style sheet. 1.353 + * 1.354 + * @param boolean immediate 1.355 + * Optional. If true the update is performed immediately. 1.356 + */ 1.357 + updateStyleSheet: function(immediate) { 1.358 + if (this._updateTask) { 1.359 + // cancel previous queued task not executed within throttle delay 1.360 + this._window.clearTimeout(this._updateTask); 1.361 + } 1.362 + 1.363 + if (immediate) { 1.364 + this._updateStyleSheet(); 1.365 + } else { 1.366 + this._updateTask = this._window.setTimeout(this._updateStyleSheet.bind(this), 1.367 + UPDATE_STYLESHEET_THROTTLE_DELAY); 1.368 + } 1.369 + }, 1.370 + 1.371 + /** 1.372 + * Update live style sheet according to modifications. 1.373 + */ 1.374 + _updateStyleSheet: function() { 1.375 + if (this.styleSheet.disabled) { 1.376 + return; // TODO: do we want to do this? 1.377 + } 1.378 + 1.379 + this._updateTask = null; // reset only if we actually perform an update 1.380 + // (stylesheet is enabled) so that 'missed' updates 1.381 + // while the stylesheet is disabled can be performed 1.382 + // when it is enabled back. @see enableStylesheet 1.383 + 1.384 + if (this.sourceEditor) { 1.385 + this._state.text = this.sourceEditor.getText(); 1.386 + } 1.387 + 1.388 + this.styleSheet.update(this._state.text, true); 1.389 + }, 1.390 + 1.391 + /** 1.392 + * Save the editor contents into a file and set savedFile property. 1.393 + * A file picker UI will open if file is not set and editor is not headless. 1.394 + * 1.395 + * @param mixed file 1.396 + * Optional nsIFile or string representing the filename to save in the 1.397 + * background, no UI will be displayed. 1.398 + * If not specified, the original style sheet URI is used. 1.399 + * To implement 'Save' instead of 'Save as', you can pass savedFile here. 1.400 + * @param function(nsIFile aFile) callback 1.401 + * Optional callback called when the operation has finished. 1.402 + * aFile has the nsIFile object for saved file or null if the operation 1.403 + * has failed or has been canceled by the user. 1.404 + * @see savedFile 1.405 + */ 1.406 + saveToFile: function(file, callback) { 1.407 + let onFile = (returnFile) => { 1.408 + if (!returnFile) { 1.409 + if (callback) { 1.410 + callback(null); 1.411 + } 1.412 + return; 1.413 + } 1.414 + 1.415 + if (this.sourceEditor) { 1.416 + this._state.text = this.sourceEditor.getText(); 1.417 + } 1.418 + 1.419 + let ostream = FileUtils.openSafeFileOutputStream(returnFile); 1.420 + let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] 1.421 + .createInstance(Ci.nsIScriptableUnicodeConverter); 1.422 + converter.charset = "UTF-8"; 1.423 + let istream = converter.convertToInputStream(this._state.text); 1.424 + 1.425 + NetUtil.asyncCopy(istream, ostream, function onStreamCopied(status) { 1.426 + if (!Components.isSuccessCode(status)) { 1.427 + if (callback) { 1.428 + callback(null); 1.429 + } 1.430 + this.emit("error", SAVE_ERROR); 1.431 + return; 1.432 + } 1.433 + FileUtils.closeSafeFileOutputStream(ostream); 1.434 + 1.435 + this.onFileSaved(returnFile); 1.436 + 1.437 + if (callback) { 1.438 + callback(returnFile); 1.439 + } 1.440 + }.bind(this)); 1.441 + }; 1.442 + 1.443 + let defaultName; 1.444 + if (this._friendlyName) { 1.445 + defaultName = OS.Path.basename(this._friendlyName); 1.446 + } 1.447 + showFilePicker(file || this._styleSheetFilePath, true, this._window, 1.448 + onFile, defaultName); 1.449 + }, 1.450 + 1.451 + /** 1.452 + * Called when this source has been successfully saved to disk. 1.453 + */ 1.454 + onFileSaved: function(returnFile) { 1.455 + this._friendlyName = null; 1.456 + this.savedFile = returnFile; 1.457 + 1.458 + this.sourceEditor.setClean(); 1.459 + 1.460 + this.emit("property-change"); 1.461 + 1.462 + // TODO: replace with file watching 1.463 + this._modCheckCount = 0; 1.464 + this._window.clearTimeout(this._timeout); 1.465 + 1.466 + if (this.linkedCSSFile && !this.linkedCSSFileError) { 1.467 + this._timeout = this._window.setTimeout(this.checkLinkedFileForChanges, 1.468 + CHECK_LINKED_SHEET_DELAY); 1.469 + } 1.470 + }, 1.471 + 1.472 + /** 1.473 + * Check to see if our linked CSS file has changed on disk, and 1.474 + * if so, update the live style sheet. 1.475 + */ 1.476 + checkLinkedFileForChanges: function() { 1.477 + OS.File.stat(this.linkedCSSFile).then((info) => { 1.478 + let lastChange = info.lastModificationDate.getTime(); 1.479 + 1.480 + if (this._fileModDate && lastChange != this._fileModDate) { 1.481 + this._fileModDate = lastChange; 1.482 + this._modCheckCount = 0; 1.483 + 1.484 + this.updateLinkedStyleSheet(); 1.485 + return; 1.486 + } 1.487 + 1.488 + if (++this._modCheckCount > MAX_CHECK_COUNT) { 1.489 + this.updateLinkedStyleSheet(); 1.490 + return; 1.491 + } 1.492 + 1.493 + // try again in a bit 1.494 + this._timeout = this._window.setTimeout(this.checkLinkedFileForChanges, 1.495 + CHECK_LINKED_SHEET_DELAY); 1.496 + }, this.markLinkedFileBroken); 1.497 + }, 1.498 + 1.499 + /** 1.500 + * Notify that the linked CSS file (if this is an original source) 1.501 + * doesn't exist on disk in the place we think it does. 1.502 + * 1.503 + * @param string error 1.504 + * The error we got when trying to access the file. 1.505 + */ 1.506 + markLinkedFileBroken: function(error) { 1.507 + this.linkedCSSFileError = error || true; 1.508 + this.emit("linked-css-file-error"); 1.509 + 1.510 + error += " querying " + this.linkedCSSFile + 1.511 + " original source location: " + this.savedFile.path 1.512 + Cu.reportError(error); 1.513 + }, 1.514 + 1.515 + /** 1.516 + * For original sources (e.g. Sass files). Fetch contents of linked CSS 1.517 + * file from disk and live update the stylesheet object with the contents. 1.518 + */ 1.519 + updateLinkedStyleSheet: function() { 1.520 + OS.File.read(this.linkedCSSFile).then((array) => { 1.521 + let decoder = new TextDecoder(); 1.522 + let text = decoder.decode(array); 1.523 + 1.524 + let relatedSheet = this.styleSheet.relatedStyleSheet; 1.525 + relatedSheet.update(text, true); 1.526 + }, this.markLinkedFileBroken); 1.527 + }, 1.528 + 1.529 + /** 1.530 + * Retrieve custom key bindings objects as expected by Editor. 1.531 + * Editor action names are not displayed to the user. 1.532 + * 1.533 + * @return {array} key binding objects for the source editor 1.534 + */ 1.535 + _getKeyBindings: function() { 1.536 + let bindings = {}; 1.537 + 1.538 + bindings[Editor.accel(_("saveStyleSheet.commandkey"))] = () => { 1.539 + this.saveToFile(this.savedFile); 1.540 + }; 1.541 + 1.542 + bindings["Shift-" + Editor.accel(_("saveStyleSheet.commandkey"))] = () => { 1.543 + this.saveToFile(); 1.544 + }; 1.545 + 1.546 + return bindings; 1.547 + }, 1.548 + 1.549 + /** 1.550 + * Clean up for this editor. 1.551 + */ 1.552 + destroy: function() { 1.553 + if (this.sourceEditor) { 1.554 + this.sourceEditor.destroy(); 1.555 + } 1.556 + this.styleSheet.off("property-change", this._onPropertyChange); 1.557 + this.styleSheet.off("error", this._onError); 1.558 + } 1.559 +} 1.560 + 1.561 + 1.562 +const TAB_CHARS = "\t"; 1.563 + 1.564 +const CURRENT_OS = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS; 1.565 +const LINE_SEPARATOR = CURRENT_OS === "WINNT" ? "\r\n" : "\n"; 1.566 + 1.567 +/** 1.568 + * Prettify minified CSS text. 1.569 + * This prettifies CSS code where there is no indentation in usual places while 1.570 + * keeping original indentation as-is elsewhere. 1.571 + * 1.572 + * @param string text 1.573 + * The CSS source to prettify. 1.574 + * @return string 1.575 + * Prettified CSS source 1.576 + */ 1.577 +function prettifyCSS(text, ruleCount) 1.578 +{ 1.579 + // remove initial and terminating HTML comments and surrounding whitespace 1.580 + text = text.replace(/(?:^\s*<!--[\r\n]*)|(?:\s*-->\s*$)/g, ""); 1.581 + 1.582 + // don't attempt to prettify if there's more than one line per rule. 1.583 + let lineCount = text.split("\n").length - 1; 1.584 + if (ruleCount !== null && lineCount >= ruleCount) { 1.585 + return text; 1.586 + } 1.587 + 1.588 + let parts = []; // indented parts 1.589 + let partStart = 0; // start offset of currently parsed part 1.590 + let indent = ""; 1.591 + let indentLevel = 0; 1.592 + 1.593 + for (let i = 0; i < text.length; i++) { 1.594 + let c = text[i]; 1.595 + let shouldIndent = false; 1.596 + 1.597 + switch (c) { 1.598 + case "}": 1.599 + if (i - partStart > 1) { 1.600 + // there's more than just } on the line, add line 1.601 + parts.push(indent + text.substring(partStart, i)); 1.602 + partStart = i; 1.603 + } 1.604 + indent = TAB_CHARS.repeat(--indentLevel); 1.605 + /* fallthrough */ 1.606 + case ";": 1.607 + case "{": 1.608 + shouldIndent = true; 1.609 + break; 1.610 + } 1.611 + 1.612 + if (shouldIndent) { 1.613 + let la = text[i+1]; // one-character lookahead 1.614 + if (!/\n/.test(la) || /^\s+$/.test(text.substring(i+1, text.length))) { 1.615 + // following character should be a new line, but isn't, 1.616 + // or it's whitespace at the end of the file 1.617 + parts.push(indent + text.substring(partStart, i + 1)); 1.618 + if (c == "}") { 1.619 + parts.push(""); // for extra line separator 1.620 + } 1.621 + partStart = i + 1; 1.622 + } else { 1.623 + return text; // assume it is not minified, early exit 1.624 + } 1.625 + } 1.626 + 1.627 + if (c == "{") { 1.628 + indent = TAB_CHARS.repeat(++indentLevel); 1.629 + } 1.630 + } 1.631 + return parts.join(LINE_SEPARATOR); 1.632 +} 1.633 + 1.634 +/** 1.635 + * Find a path on disk for a file given it's hosted uri, the uri of the 1.636 + * original resource that generated it (e.g. Sass file), and the location of the 1.637 + * local file for that source. 1.638 + * 1.639 + * @param {nsIURI} uri 1.640 + * The uri of the resource 1.641 + * @param {nsIURI} origUri 1.642 + * The uri of the original source for the resource 1.643 + * @param {nsIFile} file 1.644 + * The local file for the resource on disk 1.645 + * 1.646 + * @return {string} 1.647 + * The path of original file on disk 1.648 + */ 1.649 +function findLinkedFilePath(uri, origUri, file) { 1.650 + let { origBranch, branch } = findUnsharedBranches(origUri, uri); 1.651 + let project = findProjectPath(file, origBranch); 1.652 + 1.653 + let parts = project.concat(branch); 1.654 + let path = OS.Path.join.apply(this, parts); 1.655 + 1.656 + return path; 1.657 +} 1.658 + 1.659 +/** 1.660 + * Find the path of a project given a file in the project and its branch 1.661 + * off the root. e.g.: 1.662 + * /Users/moz/proj/src/a.css" and "src/a.css" 1.663 + * would yield ["Users", "moz", "proj"] 1.664 + * 1.665 + * @param {nsIFile} file 1.666 + * file for that resource on disk 1.667 + * @param {array} branch 1.668 + * path parts for branch to chop off file path. 1.669 + * @return {array} 1.670 + * array of path parts 1.671 + */ 1.672 +function findProjectPath(file, branch) { 1.673 + let path = OS.Path.split(file.path).components; 1.674 + 1.675 + for (let i = 2; i <= branch.length; i++) { 1.676 + // work backwards until we find a differing directory name 1.677 + if (path[path.length - i] != branch[branch.length - i]) { 1.678 + return path.slice(0, path.length - i + 1); 1.679 + } 1.680 + } 1.681 + 1.682 + // if we don't find a differing directory, just chop off the branch 1.683 + return path.slice(0, path.length - branch.length); 1.684 +} 1.685 + 1.686 +/** 1.687 + * Find the parts of a uri past the root it shares with another uri. e.g: 1.688 + * "http://localhost/built/a.scss" and "http://localhost/src/a.css" 1.689 + * would yield ["built", "a.scss"] and ["src", "a.css"] 1.690 + * 1.691 + * @param {nsIURI} origUri 1.692 + * uri to find unshared branch of. Usually is uri for original source. 1.693 + * @param {nsIURI} uri 1.694 + * uri to compare against to get a shared root 1.695 + * @return {object} 1.696 + * object with 'branch' and 'origBranch' array of path parts for branch 1.697 + */ 1.698 +function findUnsharedBranches(origUri, uri) { 1.699 + origUri = OS.Path.split(origUri.path).components; 1.700 + uri = OS.Path.split(uri.path).components; 1.701 + 1.702 + for (let i = 0; i < uri.length - 1; i++) { 1.703 + if (uri[i] != origUri[i]) { 1.704 + return { 1.705 + branch: uri.slice(i), 1.706 + origBranch: origUri.slice(i) 1.707 + }; 1.708 + } 1.709 + } 1.710 + return { 1.711 + branch: uri, 1.712 + origBranch: origUri 1.713 + }; 1.714 +} 1.715 + 1.716 +/** 1.717 + * Remove the query string from a url. 1.718 + * 1.719 + * @param {string} href 1.720 + * Url to remove query string from 1.721 + * @return {string} 1.722 + * Url without query string 1.723 + */ 1.724 +function removeQuery(href) { 1.725 + return href.replace(/\?.*/, ""); 1.726 +}