|
1 /* vim:set ts=2 sw=2 sts=2 et: */ |
|
2 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
3 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
5 |
|
6 "use strict"; |
|
7 |
|
8 this.EXPORTED_SYMBOLS = ["StyleSheetEditor"]; |
|
9 |
|
10 const Cc = Components.classes; |
|
11 const Ci = Components.interfaces; |
|
12 const Cu = Components.utils; |
|
13 |
|
14 const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require; |
|
15 const Editor = require("devtools/sourceeditor/editor"); |
|
16 const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); |
|
17 const {CssLogic} = require("devtools/styleinspector/css-logic"); |
|
18 const AutoCompleter = require("devtools/sourceeditor/autocomplete"); |
|
19 |
|
20 Cu.import("resource://gre/modules/Services.jsm"); |
|
21 Cu.import("resource://gre/modules/FileUtils.jsm"); |
|
22 Cu.import("resource://gre/modules/NetUtil.jsm"); |
|
23 Cu.import("resource://gre/modules/osfile.jsm"); |
|
24 Cu.import("resource://gre/modules/devtools/event-emitter.js"); |
|
25 Cu.import("resource:///modules/devtools/StyleEditorUtil.jsm"); |
|
26 |
|
27 const LOAD_ERROR = "error-load"; |
|
28 const SAVE_ERROR = "error-save"; |
|
29 |
|
30 // max update frequency in ms (avoid potential typing lag and/or flicker) |
|
31 // @see StyleEditor.updateStylesheet |
|
32 const UPDATE_STYLESHEET_THROTTLE_DELAY = 500; |
|
33 |
|
34 // Pref which decides if CSS autocompletion is enabled in Style Editor or not. |
|
35 const AUTOCOMPLETION_PREF = "devtools.styleeditor.autocompletion-enabled"; |
|
36 |
|
37 // How long to wait to update linked CSS file after original source was saved |
|
38 // to disk. Time in ms. |
|
39 const CHECK_LINKED_SHEET_DELAY=500; |
|
40 |
|
41 // How many times to check for linked file changes |
|
42 const MAX_CHECK_COUNT=10; |
|
43 |
|
44 /** |
|
45 * StyleSheetEditor controls the editor linked to a particular StyleSheet |
|
46 * object. |
|
47 * |
|
48 * Emits events: |
|
49 * 'property-change': A property on the underlying stylesheet has changed |
|
50 * 'source-editor-load': The source editor for this editor has been loaded |
|
51 * 'error': An error has occured |
|
52 * |
|
53 * @param {StyleSheet|OriginalSource} styleSheet |
|
54 * Stylesheet or original source to show |
|
55 * @param {DOMWindow} win |
|
56 * panel window for style editor |
|
57 * @param {nsIFile} file |
|
58 * Optional file that the sheet was imported from |
|
59 * @param {boolean} isNew |
|
60 * Optional whether the sheet was created by the user |
|
61 * @param {Walker} walker |
|
62 * Optional walker used for selectors autocompletion |
|
63 */ |
|
64 function StyleSheetEditor(styleSheet, win, file, isNew, walker) { |
|
65 EventEmitter.decorate(this); |
|
66 |
|
67 this.styleSheet = styleSheet; |
|
68 this._inputElement = null; |
|
69 this.sourceEditor = null; |
|
70 this._window = win; |
|
71 this._isNew = isNew; |
|
72 this.walker = walker; |
|
73 |
|
74 this._state = { // state to use when inputElement attaches |
|
75 text: "", |
|
76 selection: { |
|
77 start: {line: 0, ch: 0}, |
|
78 end: {line: 0, ch: 0} |
|
79 }, |
|
80 topIndex: 0 // the first visible line |
|
81 }; |
|
82 |
|
83 this._styleSheetFilePath = null; |
|
84 if (styleSheet.href && |
|
85 Services.io.extractScheme(this.styleSheet.href) == "file") { |
|
86 this._styleSheetFilePath = this.styleSheet.href; |
|
87 } |
|
88 |
|
89 this._onPropertyChange = this._onPropertyChange.bind(this); |
|
90 this._onError = this._onError.bind(this); |
|
91 this.checkLinkedFileForChanges = this.checkLinkedFileForChanges.bind(this); |
|
92 this.markLinkedFileBroken = this.markLinkedFileBroken.bind(this); |
|
93 |
|
94 this._focusOnSourceEditorReady = false; |
|
95 |
|
96 let relatedSheet = this.styleSheet.relatedStyleSheet; |
|
97 if (relatedSheet) { |
|
98 relatedSheet.on("property-change", this._onPropertyChange); |
|
99 } |
|
100 this.styleSheet.on("property-change", this._onPropertyChange); |
|
101 this.styleSheet.on("error", this._onError); |
|
102 |
|
103 this.savedFile = file; |
|
104 this.linkCSSFile(); |
|
105 } |
|
106 |
|
107 StyleSheetEditor.prototype = { |
|
108 /** |
|
109 * Whether there are unsaved changes in the editor |
|
110 */ |
|
111 get unsaved() { |
|
112 return this.sourceEditor && !this.sourceEditor.isClean(); |
|
113 }, |
|
114 |
|
115 /** |
|
116 * Whether the editor is for a stylesheet created by the user |
|
117 * through the style editor UI. |
|
118 */ |
|
119 get isNew() { |
|
120 return this._isNew; |
|
121 }, |
|
122 |
|
123 get savedFile() { |
|
124 return this._savedFile; |
|
125 }, |
|
126 |
|
127 set savedFile(name) { |
|
128 this._savedFile = name; |
|
129 |
|
130 this.linkCSSFile(); |
|
131 }, |
|
132 |
|
133 /** |
|
134 * Get a user-friendly name for the style sheet. |
|
135 * |
|
136 * @return string |
|
137 */ |
|
138 get friendlyName() { |
|
139 if (this.savedFile) { |
|
140 return this.savedFile.leafName; |
|
141 } |
|
142 |
|
143 if (this._isNew) { |
|
144 let index = this.styleSheet.styleSheetIndex + 1; |
|
145 return _("newStyleSheet", index); |
|
146 } |
|
147 |
|
148 if (!this.styleSheet.href) { |
|
149 let index = this.styleSheet.styleSheetIndex + 1; |
|
150 return _("inlineStyleSheet", index); |
|
151 } |
|
152 |
|
153 if (!this._friendlyName) { |
|
154 let sheetURI = this.styleSheet.href; |
|
155 this._friendlyName = CssLogic.shortSource({ href: sheetURI }); |
|
156 try { |
|
157 this._friendlyName = decodeURI(this._friendlyName); |
|
158 } catch (ex) { |
|
159 } |
|
160 } |
|
161 return this._friendlyName; |
|
162 }, |
|
163 |
|
164 /** |
|
165 * If this is an original source, get the path of the CSS file it generated. |
|
166 */ |
|
167 linkCSSFile: function() { |
|
168 if (!this.styleSheet.isOriginalSource) { |
|
169 return; |
|
170 } |
|
171 |
|
172 let relatedSheet = this.styleSheet.relatedStyleSheet; |
|
173 |
|
174 let path; |
|
175 let href = removeQuery(relatedSheet.href); |
|
176 let uri = NetUtil.newURI(href); |
|
177 |
|
178 if (uri.scheme == "file") { |
|
179 let file = uri.QueryInterface(Ci.nsIFileURL).file; |
|
180 path = file.path; |
|
181 } |
|
182 else if (this.savedFile) { |
|
183 let origHref = removeQuery(this.styleSheet.href); |
|
184 let origUri = NetUtil.newURI(origHref); |
|
185 path = findLinkedFilePath(uri, origUri, this.savedFile); |
|
186 } |
|
187 else { |
|
188 // we can't determine path to generated file on disk |
|
189 return; |
|
190 } |
|
191 |
|
192 if (this.linkedCSSFile == path) { |
|
193 return; |
|
194 } |
|
195 |
|
196 this.linkedCSSFile = path; |
|
197 |
|
198 this.linkedCSSFileError = null; |
|
199 |
|
200 // save last file change time so we can compare when we check for changes. |
|
201 OS.File.stat(path).then((info) => { |
|
202 this._fileModDate = info.lastModificationDate.getTime(); |
|
203 }, this.markLinkedFileBroken); |
|
204 |
|
205 this.emit("linked-css-file"); |
|
206 }, |
|
207 |
|
208 /** |
|
209 * Start fetching the full text source for this editor's sheet. |
|
210 */ |
|
211 fetchSource: function(callback) { |
|
212 this.styleSheet.getText().then((longStr) => { |
|
213 longStr.string().then((source) => { |
|
214 let ruleCount = this.styleSheet.ruleCount; |
|
215 this._state.text = prettifyCSS(source, ruleCount); |
|
216 this.sourceLoaded = true; |
|
217 |
|
218 callback(source); |
|
219 }); |
|
220 }, e => { |
|
221 this.emit("error", LOAD_ERROR, this.styleSheet.href); |
|
222 }) |
|
223 }, |
|
224 |
|
225 /** |
|
226 * Forward property-change event from stylesheet. |
|
227 * |
|
228 * @param {string} event |
|
229 * Event type |
|
230 * @param {string} property |
|
231 * Property that has changed on sheet |
|
232 */ |
|
233 _onPropertyChange: function(property, value) { |
|
234 this.emit("property-change", property, value); |
|
235 }, |
|
236 |
|
237 /** |
|
238 * Forward error event from stylesheet. |
|
239 * |
|
240 * @param {string} event |
|
241 * Event type |
|
242 * @param {string} errorCode |
|
243 */ |
|
244 _onError: function(event, errorCode) { |
|
245 this.emit("error", errorCode); |
|
246 }, |
|
247 |
|
248 /** |
|
249 * Create source editor and load state into it. |
|
250 * @param {DOMElement} inputElement |
|
251 * Element to load source editor in |
|
252 * |
|
253 * @return {Promise} |
|
254 * Promise that will resolve when the style editor is loaded. |
|
255 */ |
|
256 load: function(inputElement) { |
|
257 this._inputElement = inputElement; |
|
258 |
|
259 let config = { |
|
260 value: this._state.text, |
|
261 lineNumbers: true, |
|
262 mode: Editor.modes.css, |
|
263 readOnly: false, |
|
264 autoCloseBrackets: "{}()[]", |
|
265 extraKeys: this._getKeyBindings(), |
|
266 contextMenu: "sourceEditorContextMenu" |
|
267 }; |
|
268 let sourceEditor = new Editor(config); |
|
269 |
|
270 sourceEditor.on("dirty-change", this._onPropertyChange); |
|
271 |
|
272 return sourceEditor.appendTo(inputElement).then(() => { |
|
273 if (Services.prefs.getBoolPref(AUTOCOMPLETION_PREF)) { |
|
274 sourceEditor.extend(AutoCompleter); |
|
275 sourceEditor.setupAutoCompletion(this.walker); |
|
276 } |
|
277 sourceEditor.on("save", () => { |
|
278 this.saveToFile(); |
|
279 }); |
|
280 |
|
281 if (this.styleSheet.update) { |
|
282 sourceEditor.on("change", () => { |
|
283 this.updateStyleSheet(); |
|
284 }); |
|
285 } |
|
286 |
|
287 this.sourceEditor = sourceEditor; |
|
288 |
|
289 if (this._focusOnSourceEditorReady) { |
|
290 this._focusOnSourceEditorReady = false; |
|
291 sourceEditor.focus(); |
|
292 } |
|
293 |
|
294 sourceEditor.setFirstVisibleLine(this._state.topIndex); |
|
295 sourceEditor.setSelection(this._state.selection.start, |
|
296 this._state.selection.end); |
|
297 |
|
298 this.emit("source-editor-load"); |
|
299 }); |
|
300 }, |
|
301 |
|
302 /** |
|
303 * Get the source editor for this editor. |
|
304 * |
|
305 * @return {Promise} |
|
306 * Promise that will resolve with the editor. |
|
307 */ |
|
308 getSourceEditor: function() { |
|
309 let deferred = promise.defer(); |
|
310 |
|
311 if (this.sourceEditor) { |
|
312 return promise.resolve(this); |
|
313 } |
|
314 this.on("source-editor-load", () => { |
|
315 deferred.resolve(this); |
|
316 }); |
|
317 return deferred.promise; |
|
318 }, |
|
319 |
|
320 /** |
|
321 * Focus the Style Editor input. |
|
322 */ |
|
323 focus: function() { |
|
324 if (this.sourceEditor) { |
|
325 this.sourceEditor.focus(); |
|
326 } else { |
|
327 this._focusOnSourceEditorReady = true; |
|
328 } |
|
329 }, |
|
330 |
|
331 /** |
|
332 * Event handler for when the editor is shown. |
|
333 */ |
|
334 onShow: function() { |
|
335 if (this.sourceEditor) { |
|
336 this.sourceEditor.setFirstVisibleLine(this._state.topIndex); |
|
337 } |
|
338 this.focus(); |
|
339 }, |
|
340 |
|
341 /** |
|
342 * Toggled the disabled state of the underlying stylesheet. |
|
343 */ |
|
344 toggleDisabled: function() { |
|
345 this.styleSheet.toggleDisabled(); |
|
346 }, |
|
347 |
|
348 /** |
|
349 * Queue a throttled task to update the live style sheet. |
|
350 * |
|
351 * @param boolean immediate |
|
352 * Optional. If true the update is performed immediately. |
|
353 */ |
|
354 updateStyleSheet: function(immediate) { |
|
355 if (this._updateTask) { |
|
356 // cancel previous queued task not executed within throttle delay |
|
357 this._window.clearTimeout(this._updateTask); |
|
358 } |
|
359 |
|
360 if (immediate) { |
|
361 this._updateStyleSheet(); |
|
362 } else { |
|
363 this._updateTask = this._window.setTimeout(this._updateStyleSheet.bind(this), |
|
364 UPDATE_STYLESHEET_THROTTLE_DELAY); |
|
365 } |
|
366 }, |
|
367 |
|
368 /** |
|
369 * Update live style sheet according to modifications. |
|
370 */ |
|
371 _updateStyleSheet: function() { |
|
372 if (this.styleSheet.disabled) { |
|
373 return; // TODO: do we want to do this? |
|
374 } |
|
375 |
|
376 this._updateTask = null; // reset only if we actually perform an update |
|
377 // (stylesheet is enabled) so that 'missed' updates |
|
378 // while the stylesheet is disabled can be performed |
|
379 // when it is enabled back. @see enableStylesheet |
|
380 |
|
381 if (this.sourceEditor) { |
|
382 this._state.text = this.sourceEditor.getText(); |
|
383 } |
|
384 |
|
385 this.styleSheet.update(this._state.text, true); |
|
386 }, |
|
387 |
|
388 /** |
|
389 * Save the editor contents into a file and set savedFile property. |
|
390 * A file picker UI will open if file is not set and editor is not headless. |
|
391 * |
|
392 * @param mixed file |
|
393 * Optional nsIFile or string representing the filename to save in the |
|
394 * background, no UI will be displayed. |
|
395 * If not specified, the original style sheet URI is used. |
|
396 * To implement 'Save' instead of 'Save as', you can pass savedFile here. |
|
397 * @param function(nsIFile aFile) callback |
|
398 * Optional callback called when the operation has finished. |
|
399 * aFile has the nsIFile object for saved file or null if the operation |
|
400 * has failed or has been canceled by the user. |
|
401 * @see savedFile |
|
402 */ |
|
403 saveToFile: function(file, callback) { |
|
404 let onFile = (returnFile) => { |
|
405 if (!returnFile) { |
|
406 if (callback) { |
|
407 callback(null); |
|
408 } |
|
409 return; |
|
410 } |
|
411 |
|
412 if (this.sourceEditor) { |
|
413 this._state.text = this.sourceEditor.getText(); |
|
414 } |
|
415 |
|
416 let ostream = FileUtils.openSafeFileOutputStream(returnFile); |
|
417 let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] |
|
418 .createInstance(Ci.nsIScriptableUnicodeConverter); |
|
419 converter.charset = "UTF-8"; |
|
420 let istream = converter.convertToInputStream(this._state.text); |
|
421 |
|
422 NetUtil.asyncCopy(istream, ostream, function onStreamCopied(status) { |
|
423 if (!Components.isSuccessCode(status)) { |
|
424 if (callback) { |
|
425 callback(null); |
|
426 } |
|
427 this.emit("error", SAVE_ERROR); |
|
428 return; |
|
429 } |
|
430 FileUtils.closeSafeFileOutputStream(ostream); |
|
431 |
|
432 this.onFileSaved(returnFile); |
|
433 |
|
434 if (callback) { |
|
435 callback(returnFile); |
|
436 } |
|
437 }.bind(this)); |
|
438 }; |
|
439 |
|
440 let defaultName; |
|
441 if (this._friendlyName) { |
|
442 defaultName = OS.Path.basename(this._friendlyName); |
|
443 } |
|
444 showFilePicker(file || this._styleSheetFilePath, true, this._window, |
|
445 onFile, defaultName); |
|
446 }, |
|
447 |
|
448 /** |
|
449 * Called when this source has been successfully saved to disk. |
|
450 */ |
|
451 onFileSaved: function(returnFile) { |
|
452 this._friendlyName = null; |
|
453 this.savedFile = returnFile; |
|
454 |
|
455 this.sourceEditor.setClean(); |
|
456 |
|
457 this.emit("property-change"); |
|
458 |
|
459 // TODO: replace with file watching |
|
460 this._modCheckCount = 0; |
|
461 this._window.clearTimeout(this._timeout); |
|
462 |
|
463 if (this.linkedCSSFile && !this.linkedCSSFileError) { |
|
464 this._timeout = this._window.setTimeout(this.checkLinkedFileForChanges, |
|
465 CHECK_LINKED_SHEET_DELAY); |
|
466 } |
|
467 }, |
|
468 |
|
469 /** |
|
470 * Check to see if our linked CSS file has changed on disk, and |
|
471 * if so, update the live style sheet. |
|
472 */ |
|
473 checkLinkedFileForChanges: function() { |
|
474 OS.File.stat(this.linkedCSSFile).then((info) => { |
|
475 let lastChange = info.lastModificationDate.getTime(); |
|
476 |
|
477 if (this._fileModDate && lastChange != this._fileModDate) { |
|
478 this._fileModDate = lastChange; |
|
479 this._modCheckCount = 0; |
|
480 |
|
481 this.updateLinkedStyleSheet(); |
|
482 return; |
|
483 } |
|
484 |
|
485 if (++this._modCheckCount > MAX_CHECK_COUNT) { |
|
486 this.updateLinkedStyleSheet(); |
|
487 return; |
|
488 } |
|
489 |
|
490 // try again in a bit |
|
491 this._timeout = this._window.setTimeout(this.checkLinkedFileForChanges, |
|
492 CHECK_LINKED_SHEET_DELAY); |
|
493 }, this.markLinkedFileBroken); |
|
494 }, |
|
495 |
|
496 /** |
|
497 * Notify that the linked CSS file (if this is an original source) |
|
498 * doesn't exist on disk in the place we think it does. |
|
499 * |
|
500 * @param string error |
|
501 * The error we got when trying to access the file. |
|
502 */ |
|
503 markLinkedFileBroken: function(error) { |
|
504 this.linkedCSSFileError = error || true; |
|
505 this.emit("linked-css-file-error"); |
|
506 |
|
507 error += " querying " + this.linkedCSSFile + |
|
508 " original source location: " + this.savedFile.path |
|
509 Cu.reportError(error); |
|
510 }, |
|
511 |
|
512 /** |
|
513 * For original sources (e.g. Sass files). Fetch contents of linked CSS |
|
514 * file from disk and live update the stylesheet object with the contents. |
|
515 */ |
|
516 updateLinkedStyleSheet: function() { |
|
517 OS.File.read(this.linkedCSSFile).then((array) => { |
|
518 let decoder = new TextDecoder(); |
|
519 let text = decoder.decode(array); |
|
520 |
|
521 let relatedSheet = this.styleSheet.relatedStyleSheet; |
|
522 relatedSheet.update(text, true); |
|
523 }, this.markLinkedFileBroken); |
|
524 }, |
|
525 |
|
526 /** |
|
527 * Retrieve custom key bindings objects as expected by Editor. |
|
528 * Editor action names are not displayed to the user. |
|
529 * |
|
530 * @return {array} key binding objects for the source editor |
|
531 */ |
|
532 _getKeyBindings: function() { |
|
533 let bindings = {}; |
|
534 |
|
535 bindings[Editor.accel(_("saveStyleSheet.commandkey"))] = () => { |
|
536 this.saveToFile(this.savedFile); |
|
537 }; |
|
538 |
|
539 bindings["Shift-" + Editor.accel(_("saveStyleSheet.commandkey"))] = () => { |
|
540 this.saveToFile(); |
|
541 }; |
|
542 |
|
543 return bindings; |
|
544 }, |
|
545 |
|
546 /** |
|
547 * Clean up for this editor. |
|
548 */ |
|
549 destroy: function() { |
|
550 if (this.sourceEditor) { |
|
551 this.sourceEditor.destroy(); |
|
552 } |
|
553 this.styleSheet.off("property-change", this._onPropertyChange); |
|
554 this.styleSheet.off("error", this._onError); |
|
555 } |
|
556 } |
|
557 |
|
558 |
|
559 const TAB_CHARS = "\t"; |
|
560 |
|
561 const CURRENT_OS = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS; |
|
562 const LINE_SEPARATOR = CURRENT_OS === "WINNT" ? "\r\n" : "\n"; |
|
563 |
|
564 /** |
|
565 * Prettify minified CSS text. |
|
566 * This prettifies CSS code where there is no indentation in usual places while |
|
567 * keeping original indentation as-is elsewhere. |
|
568 * |
|
569 * @param string text |
|
570 * The CSS source to prettify. |
|
571 * @return string |
|
572 * Prettified CSS source |
|
573 */ |
|
574 function prettifyCSS(text, ruleCount) |
|
575 { |
|
576 // remove initial and terminating HTML comments and surrounding whitespace |
|
577 text = text.replace(/(?:^\s*<!--[\r\n]*)|(?:\s*-->\s*$)/g, ""); |
|
578 |
|
579 // don't attempt to prettify if there's more than one line per rule. |
|
580 let lineCount = text.split("\n").length - 1; |
|
581 if (ruleCount !== null && lineCount >= ruleCount) { |
|
582 return text; |
|
583 } |
|
584 |
|
585 let parts = []; // indented parts |
|
586 let partStart = 0; // start offset of currently parsed part |
|
587 let indent = ""; |
|
588 let indentLevel = 0; |
|
589 |
|
590 for (let i = 0; i < text.length; i++) { |
|
591 let c = text[i]; |
|
592 let shouldIndent = false; |
|
593 |
|
594 switch (c) { |
|
595 case "}": |
|
596 if (i - partStart > 1) { |
|
597 // there's more than just } on the line, add line |
|
598 parts.push(indent + text.substring(partStart, i)); |
|
599 partStart = i; |
|
600 } |
|
601 indent = TAB_CHARS.repeat(--indentLevel); |
|
602 /* fallthrough */ |
|
603 case ";": |
|
604 case "{": |
|
605 shouldIndent = true; |
|
606 break; |
|
607 } |
|
608 |
|
609 if (shouldIndent) { |
|
610 let la = text[i+1]; // one-character lookahead |
|
611 if (!/\n/.test(la) || /^\s+$/.test(text.substring(i+1, text.length))) { |
|
612 // following character should be a new line, but isn't, |
|
613 // or it's whitespace at the end of the file |
|
614 parts.push(indent + text.substring(partStart, i + 1)); |
|
615 if (c == "}") { |
|
616 parts.push(""); // for extra line separator |
|
617 } |
|
618 partStart = i + 1; |
|
619 } else { |
|
620 return text; // assume it is not minified, early exit |
|
621 } |
|
622 } |
|
623 |
|
624 if (c == "{") { |
|
625 indent = TAB_CHARS.repeat(++indentLevel); |
|
626 } |
|
627 } |
|
628 return parts.join(LINE_SEPARATOR); |
|
629 } |
|
630 |
|
631 /** |
|
632 * Find a path on disk for a file given it's hosted uri, the uri of the |
|
633 * original resource that generated it (e.g. Sass file), and the location of the |
|
634 * local file for that source. |
|
635 * |
|
636 * @param {nsIURI} uri |
|
637 * The uri of the resource |
|
638 * @param {nsIURI} origUri |
|
639 * The uri of the original source for the resource |
|
640 * @param {nsIFile} file |
|
641 * The local file for the resource on disk |
|
642 * |
|
643 * @return {string} |
|
644 * The path of original file on disk |
|
645 */ |
|
646 function findLinkedFilePath(uri, origUri, file) { |
|
647 let { origBranch, branch } = findUnsharedBranches(origUri, uri); |
|
648 let project = findProjectPath(file, origBranch); |
|
649 |
|
650 let parts = project.concat(branch); |
|
651 let path = OS.Path.join.apply(this, parts); |
|
652 |
|
653 return path; |
|
654 } |
|
655 |
|
656 /** |
|
657 * Find the path of a project given a file in the project and its branch |
|
658 * off the root. e.g.: |
|
659 * /Users/moz/proj/src/a.css" and "src/a.css" |
|
660 * would yield ["Users", "moz", "proj"] |
|
661 * |
|
662 * @param {nsIFile} file |
|
663 * file for that resource on disk |
|
664 * @param {array} branch |
|
665 * path parts for branch to chop off file path. |
|
666 * @return {array} |
|
667 * array of path parts |
|
668 */ |
|
669 function findProjectPath(file, branch) { |
|
670 let path = OS.Path.split(file.path).components; |
|
671 |
|
672 for (let i = 2; i <= branch.length; i++) { |
|
673 // work backwards until we find a differing directory name |
|
674 if (path[path.length - i] != branch[branch.length - i]) { |
|
675 return path.slice(0, path.length - i + 1); |
|
676 } |
|
677 } |
|
678 |
|
679 // if we don't find a differing directory, just chop off the branch |
|
680 return path.slice(0, path.length - branch.length); |
|
681 } |
|
682 |
|
683 /** |
|
684 * Find the parts of a uri past the root it shares with another uri. e.g: |
|
685 * "http://localhost/built/a.scss" and "http://localhost/src/a.css" |
|
686 * would yield ["built", "a.scss"] and ["src", "a.css"] |
|
687 * |
|
688 * @param {nsIURI} origUri |
|
689 * uri to find unshared branch of. Usually is uri for original source. |
|
690 * @param {nsIURI} uri |
|
691 * uri to compare against to get a shared root |
|
692 * @return {object} |
|
693 * object with 'branch' and 'origBranch' array of path parts for branch |
|
694 */ |
|
695 function findUnsharedBranches(origUri, uri) { |
|
696 origUri = OS.Path.split(origUri.path).components; |
|
697 uri = OS.Path.split(uri.path).components; |
|
698 |
|
699 for (let i = 0; i < uri.length - 1; i++) { |
|
700 if (uri[i] != origUri[i]) { |
|
701 return { |
|
702 branch: uri.slice(i), |
|
703 origBranch: origUri.slice(i) |
|
704 }; |
|
705 } |
|
706 } |
|
707 return { |
|
708 branch: uri, |
|
709 origBranch: origUri |
|
710 }; |
|
711 } |
|
712 |
|
713 /** |
|
714 * Remove the query string from a url. |
|
715 * |
|
716 * @param {string} href |
|
717 * Url to remove query string from |
|
718 * @return {string} |
|
719 * Url without query string |
|
720 */ |
|
721 function removeQuery(href) { |
|
722 return href.replace(/\?.*/, ""); |
|
723 } |