2 * Copyright (C) 2012 Google Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
8 * * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 * * Redistributions in binary form must reproduce the above
11 * copyright notice, this list of conditions and the following disclaimer
12 * in the documentation and/or other materials provided with the
14 * * Neither the name of Google Inc. nor the names of its
15 * contributors may be used to endorse or promote products derived from
16 * this software without specific prior written permission.
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33 * @implements {WebInspector.CSSSourceMapping}
34 * @param {!WebInspector.CSSStyleModel} cssModel
35 * @param {!WebInspector.Workspace} workspace
36 * @param {!WebInspector.NetworkMapping} networkMapping
37 * @param {!WebInspector.NetworkProject} networkProject
39 WebInspector.SASSSourceMapping = function(cssModel, workspace, networkMapping, networkProject)
41 this._cssModel = cssModel;
42 this._workspace = workspace;
43 this._networkProject = networkProject;
44 this._addingRevisionCounter = 0;
45 this._pollManager = new WebInspector.SASSSourceMapping.PollManager(this._cssModel, networkMapping, this._updateCSSRevision.bind(this));
47 WebInspector.fileManager.addEventListener(WebInspector.FileManager.EventTypes.SavedURL, this._fileSaveFinished, this);
48 WebInspector.moduleSetting("cssSourceMapsEnabled").addChangeListener(this._toggleSourceMapSupport, this);
49 this._cssModel.addEventListener(WebInspector.CSSStyleModel.Events.StyleSheetChanged, this._styleSheetChanged, this);
50 this._workspace.addEventListener(WebInspector.Workspace.Events.UISourceCodeAdded, this._uiSourceCodeAdded, this);
51 this._workspace.addEventListener(WebInspector.Workspace.Events.UISourceCodeContentCommitted, this._uiSourceCodeContentCommitted, this);
52 this._workspace.addEventListener(WebInspector.Workspace.Events.ProjectRemoved, this._reset, this);
53 this._networkMapping = networkMapping;
56 WebInspector.SASSSourceMapping.prototype = {
58 * @param {!WebInspector.Event} event
60 _styleSheetChanged: function(event)
62 var id = /** @type {!CSSAgent.StyleSheetId} */ (event.data.styleSheetId);
63 if (this._addingRevisionCounter) {
64 --this._addingRevisionCounter;
67 var header = this._cssModel.styleSheetHeaderForId(id);
71 this.removeHeader(header);
75 * @param {!WebInspector.Event} event
77 _toggleSourceMapSupport: function(event)
79 var enabled = /** @type {boolean} */ (event.data);
80 var headers = this._cssModel.styleSheetHeaders();
81 for (var i = 0; i < headers.length; ++i) {
83 this.addHeader(headers[i]);
85 this.removeHeader(headers[i]);
90 * @param {!WebInspector.Event} event
92 _fileSaveFinished: function(event)
94 var sassURL = /** @type {string} */ (event.data);
95 var cssURLs = this._sassURLToCSSURLs.get(sassURL).valuesArray();
96 this._pollManager.sassFileChanged(sassURL, cssURLs, false);
100 * @param {!WebInspector.UISourceCode} cssUISourceCode
101 * @param {string} content
104 _updateCSSRevision: function(cssUISourceCode, content)
106 ++this._addingRevisionCounter;
107 cssUISourceCode.addRevision(content);
108 var cssURL = this._networkMapping.networkURL(cssUISourceCode);
109 var completeSourceMapURL = this._completeSourceMapURLForCSSURL[cssURL];
110 if (!completeSourceMapURL)
112 var ids = this._cssModel.styleSheetIdsForURL(cssURL);
116 for (var i = 0; i < ids.length; ++i)
117 headers.push(this._cssModel.styleSheetHeaderForId(ids[i]));
118 this._loadSourceMapAndBindUISourceCode(headers, true, completeSourceMapURL);
123 * @param {!WebInspector.CSSStyleSheetHeader} header
125 addHeader: function(header)
127 if (!header.sourceMapURL || !header.sourceURL || !WebInspector.moduleSetting("cssSourceMapsEnabled").get())
129 var completeSourceMapURL = WebInspector.ParsedURL.completeURL(header.sourceURL, header.sourceMapURL);
130 if (!completeSourceMapURL)
132 this._completeSourceMapURLForCSSURL[header.sourceURL] = completeSourceMapURL;
133 this._loadSourceMapAndBindUISourceCode([header], false, completeSourceMapURL);
137 * @param {!WebInspector.CSSStyleSheetHeader} header
139 removeHeader: function(header)
141 var sourceURL = header.sourceURL;
142 if (!sourceURL || !header.sourceMapURL || !this._completeSourceMapURLForCSSURL[sourceURL])
144 var sourceMap = this._sourceMapByStyleSheetURL[sourceURL];
145 var sources = sourceMap.sources();
146 for (var i = 0; i < sources.length; ++i)
147 this._sassURLToCSSURLs.remove(sources[i], sourceURL);
148 delete this._sourceMapByStyleSheetURL[sourceURL];
149 delete this._completeSourceMapURLForCSSURL[sourceURL];
151 var completeSourceMapURL = WebInspector.ParsedURL.completeURL(sourceURL, header.sourceMapURL);
152 if (completeSourceMapURL)
153 delete this._sourceMapByURL[completeSourceMapURL];
154 WebInspector.cssWorkspaceBinding.updateLocations(header);
158 * @param {!Array.<!WebInspector.CSSStyleSheetHeader>} headersWithSameSourceURL
159 * @param {boolean} forceRebind
160 * @param {string} completeSourceMapURL
162 _loadSourceMapAndBindUISourceCode: function(headersWithSameSourceURL, forceRebind, completeSourceMapURL)
164 console.assert(headersWithSameSourceURL.length);
165 var sourceURL = headersWithSameSourceURL[0].sourceURL;
166 this._loadSourceMapForStyleSheet(completeSourceMapURL, sourceURL, forceRebind, sourceMapLoaded.bind(this));
169 * @param {?WebInspector.SourceMap} sourceMap
170 * @this {WebInspector.SASSSourceMapping}
172 function sourceMapLoaded(sourceMap)
177 this._sourceMapByStyleSheetURL[sourceURL] = sourceMap;
178 for (var i = 0; i < headersWithSameSourceURL.length; ++i) {
180 WebInspector.cssWorkspaceBinding.updateLocations(headersWithSameSourceURL[i]);
182 this._bindUISourceCode(headersWithSameSourceURL[i], sourceMap);
188 * @param {string} completeSourceMapURL
189 * @param {string} completeStyleSheetURL
190 * @param {boolean} forceReload
191 * @param {function(?WebInspector.SourceMap)} callback
193 _loadSourceMapForStyleSheet: function(completeSourceMapURL, completeStyleSheetURL, forceReload, callback)
195 var sourceMap = this._sourceMapByURL[completeSourceMapURL];
196 if (sourceMap && !forceReload) {
201 var pendingCallbacks = this._pendingSourceMapLoadingCallbacks[completeSourceMapURL];
202 if (pendingCallbacks) {
203 pendingCallbacks.push(callback);
207 pendingCallbacks = [callback];
208 this._pendingSourceMapLoadingCallbacks[completeSourceMapURL] = pendingCallbacks;
210 WebInspector.SourceMap.load(completeSourceMapURL, completeStyleSheetURL, sourceMapLoaded.bind(this));
213 * @param {?WebInspector.SourceMap} sourceMap
214 * @this {WebInspector.SASSSourceMapping}
216 function sourceMapLoaded(sourceMap)
218 var callbacks = this._pendingSourceMapLoadingCallbacks[completeSourceMapURL];
219 delete this._pendingSourceMapLoadingCallbacks[completeSourceMapURL];
223 this._sourceMapByURL[completeSourceMapURL] = sourceMap;
225 delete this._sourceMapByURL[completeSourceMapURL];
226 for (var i = 0; i < callbacks.length; ++i)
227 callbacks[i](sourceMap);
232 * @param {!WebInspector.CSSStyleSheetHeader} header
233 * @param {!WebInspector.SourceMap} sourceMap
235 _bindUISourceCode: function(header, sourceMap)
237 WebInspector.cssWorkspaceBinding.pushSourceMapping(header, this);
238 var cssURL = header.sourceURL;
239 var sources = sourceMap.sources();
240 for (var i = 0; i < sources.length; ++i) {
241 var sassURL = sources[i];
242 this._sassURLToCSSURLs.set(sassURL, cssURL);
243 if (!this._networkMapping.hasMappingForURL(sassURL) && !this._networkMapping.uiSourceCodeForURL(sassURL, header.target())) {
244 var contentProvider = sourceMap.sourceContentProvider(sassURL, WebInspector.resourceTypes.Stylesheet);
245 this._networkProject.addFileForURL(sassURL, contentProvider);
252 * @param {!WebInspector.CSSLocation} rawLocation
253 * @return {?WebInspector.UILocation}
255 rawLocationToUILocation: function(rawLocation)
257 var sourceMap = this._sourceMapByStyleSheetURL[rawLocation.url];
260 var entry = sourceMap.findEntry(rawLocation.lineNumber, rawLocation.columnNumber);
261 if (!entry || !entry.sourceURL)
263 var uiSourceCode = this._networkMapping.uiSourceCodeForURL(entry.sourceURL, rawLocation.target());
266 return uiSourceCode.uiLocation(entry.sourceLineNumber, entry.sourceColumnNumber);
271 * @param {!WebInspector.UISourceCode} uiSourceCode
272 * @param {number} lineNumber
273 * @param {number} columnNumber
274 * @return {?WebInspector.CSSLocation}
276 uiLocationToRawLocation: function(uiSourceCode, lineNumber, columnNumber)
285 isIdentity: function()
292 * @param {!WebInspector.UISourceCode} uiSourceCode
293 * @param {number} lineNumber
296 uiLineHasMapping: function(uiSourceCode, lineNumber)
302 * @return {!WebInspector.Target}
306 return this._cssModel.target();
310 * @param {!WebInspector.Event} event
312 _uiSourceCodeAdded: function(event)
314 var uiSourceCode = /** @type {!WebInspector.UISourceCode} */ (event.data);
315 var networkURL = this._networkMapping.networkURL(uiSourceCode);
316 var cssURLs = this._sassURLToCSSURLs.get(networkURL).valuesArray();
319 for (var i = 0; i < cssURLs.length; ++i) {
320 var ids = this._cssModel.styleSheetIdsForURL(cssURLs[i]);
321 for (var j = 0; j < ids.length; ++j) {
322 var header = this._cssModel.styleSheetHeaderForId(ids[j]);
323 console.assert(header);
324 WebInspector.cssWorkspaceBinding.updateLocations(/** @type {!WebInspector.CSSStyleSheetHeader} */ (header));
330 * @param {!WebInspector.Event} event
332 _uiSourceCodeContentCommitted: function(event)
334 var uiSourceCode = /** @type {!WebInspector.UISourceCode} */ (event.data.uiSourceCode);
335 if (uiSourceCode.project().type() === WebInspector.projectTypes.FileSystem) {
336 var networkURL = this._networkMapping.networkURL(uiSourceCode);
337 var cssURLs = this._sassURLToCSSURLs.get(networkURL).valuesArray();
338 this._pollManager.sassFileChanged(networkURL, cssURLs, true);
344 this._addingRevisionCounter = 0;
345 this._completeSourceMapURLForCSSURL = {};
346 /** @type {!Multimap<string, string>} */
347 this._sassURLToCSSURLs = new Multimap();
348 /** @type {!Object.<string, !Array.<function(?WebInspector.SourceMap)>>} */
349 this._pendingSourceMapLoadingCallbacks = {};
350 /** @type {!Object.<string, !WebInspector.SourceMap>} */
351 this._sourceMapByURL = {};
352 this._sourceMapByStyleSheetURL = {};
353 this._pollManager.reset();
359 * @param {!WebInspector.CSSStyleModel} cssModel
360 * @param {!WebInspector.NetworkMapping} networkMapping
361 * @param {function(!WebInspector.UISourceCode, string):boolean} callback
363 WebInspector.SASSSourceMapping.PollManager = function(cssModel, networkMapping, callback)
365 this.pollPeriodMs = 30 * 1000;
366 this.pollIntervalMs = 200;
367 this._networkMapping = networkMapping;
368 this._callback = callback;
369 this._cssModel = cssModel;
373 WebInspector.SASSSourceMapping.PollManager.prototype = {
376 /** @type {!Object.<string, !{deadlineMs: number, dataByURL: !Object.<string, !{timer: number, previousPoll: number}>}>} */
377 this._pollDataForSASSURL = {};
381 * @param {string} headerName
382 * @param {!Object.<string, string>} headers
385 _headerValue: function(headerName, headers)
387 headerName = headerName.toLowerCase();
389 for (var name in headers) {
390 if (name.toLowerCase() === headerName) {
391 value = headers[name];
399 * @param {!Object.<string, string>} headers
402 _lastModified: function(headers)
404 var lastModifiedHeader = this._headerValue("last-modified", headers);
405 if (!lastModifiedHeader)
407 var lastModified = new Date(lastModifiedHeader);
408 if (isNaN(lastModified.getTime()))
414 * @param {!Object.<string, string>} headers
415 * @param {string} url
418 _checkLastModified: function(headers, url)
420 var lastModified = this._lastModified(headers);
424 var etagMessage = this._headerValue("etag", headers) ? ", \"ETag\" response header found instead" : "";
425 var message = String.sprintf("The \"Last-Modified\" response header is missing or invalid for %s%s. The CSS auto-reload functionality will not work correctly.", url, etagMessage);
426 WebInspector.console.log(message);
431 * @param {string} sassURL
432 * @param {!Array.<string>} cssURLs
433 * @param {boolean} wasLoadedFromFileSystem
435 sassFileChanged: function(sassURL, cssURLs, wasLoadedFromFileSystem)
439 if (!WebInspector.moduleSetting("cssReloadEnabled").get())
442 var sassFile = this._networkMapping.uiSourceCodeForURL(sassURL, this._cssModel.target());
443 console.assert(sassFile);
444 if (wasLoadedFromFileSystem)
445 sassFile.requestMetadata(metadataReceived.bind(this));
447 WebInspector.ResourceLoader.loadUsingTargetUA(sassURL, null, sassLoadedViaNetwork.bind(this));
450 * @param {number} statusCode
451 * @param {!Object.<string, string>} headers
452 * @param {string} content
453 * @this {WebInspector.SASSSourceMapping.PollManager}
455 function sassLoadedViaNetwork(statusCode, headers, content)
457 if (statusCode >= 400) {
458 console.error("Could not load content for " + sassURL + " : " + "HTTP status code: " + statusCode);
461 var lastModified = this._checkLastModified(headers, sassURL);
464 metadataReceived.call(this, lastModified);
468 * @param {?Date} timestamp
469 * @this {WebInspector.SASSSourceMapping.PollManager}
471 function metadataReceived(timestamp)
476 var now = Date.now();
477 var deadlineMs = now + this.pollPeriodMs;
478 var pollData = this._pollDataForSASSURL[sassURL];
480 var dataByURL = pollData.dataByURL;
481 for (var url in dataByURL)
482 clearTimeout(dataByURL[url].timer);
484 pollData = { dataByURL: {}, deadlineMs: deadlineMs, sassTimestamp: timestamp };
485 this._pollDataForSASSURL[sassURL] = pollData;
486 for (var i = 0; i < cssURLs.length; ++i) {
487 pollData.dataByURL[cssURLs[i]] = { previousPoll: now };
488 this._pollCallback(cssURLs[i], sassURL);
494 * @param {string} cssURL
495 * @param {string} sassURL
497 _pollCallback: function(cssURL, sassURL)
500 var pollData = this._pollDataForSASSURL[sassURL];
504 if ((now = new Date().getTime()) > pollData.deadlineMs) {
505 WebInspector.console.warn(WebInspector.UIString("%s hasn't been updated in %d seconds.", cssURL, this.pollPeriodMs / 1000));
506 this._stopPolling(cssURL, sassURL);
509 var nextPoll = this.pollIntervalMs + pollData.dataByURL[cssURL].previousPoll;
510 var remainingTimeoutMs = Math.max(0, nextPoll - now);
511 pollData.dataByURL[cssURL].previousPoll = now + remainingTimeoutMs;
512 pollData.dataByURL[cssURL].timer = setTimeout(this._reloadCSS.bind(this, cssURL, sassURL), remainingTimeoutMs);
516 * @param {string} cssURL
517 * @param {string} sassURL
519 _stopPolling: function(cssURL, sassURL)
521 var pollData = this._pollDataForSASSURL[sassURL];
524 delete pollData.dataByURL[cssURL];
525 if (!Object.keys(pollData.dataByURL).length)
526 delete this._pollDataForSASSURL[sassURL];
530 * @param {string} cssURL
531 * @param {string} sassURL
533 _reloadCSS: function(cssURL, sassURL)
535 var cssUISourceCode = this._networkMapping.uiSourceCodeForURL(cssURL, this._cssModel.target());
536 if (!cssUISourceCode) {
537 WebInspector.console.warn(WebInspector.UIString("%s resource missing. Please reload the page.", cssURL));
538 this._stopPolling(cssURL, sassURL)
542 if (this._networkMapping.hasMappingForURL(sassURL))
543 this._reloadCSSFromFileSystem(cssUISourceCode, sassURL);
545 this._reloadCSSFromNetwork(cssUISourceCode, sassURL);
549 * @param {!WebInspector.UISourceCode} cssUISourceCode
550 * @param {string} sassURL
552 _reloadCSSFromNetwork: function(cssUISourceCode, sassURL)
554 var cssURL = this._networkMapping.networkURL(cssUISourceCode);
555 var data = this._pollDataForSASSURL[sassURL];
557 this._stopPolling(cssURL, sassURL);
560 var headers = { "if-modified-since": new Date(data.sassTimestamp.getTime() - 1000).toUTCString() };
561 WebInspector.ResourceLoader.loadUsingTargetUA(cssURL, headers, contentLoaded.bind(this));
564 * @param {number} statusCode
565 * @param {!Object.<string, string>} headers
566 * @param {string} content
567 * @this {WebInspector.SASSSourceMapping.PollManager}
569 function contentLoaded(statusCode, headers, content)
571 if (statusCode >= 400) {
572 console.error("Could not load content for " + cssURL + " : " + "HTTP status code: " + statusCode);
573 this._stopPolling(cssURL, sassURL);
576 if (!this._pollDataForSASSURL[sassURL]) {
577 this._stopPolling(cssURL, sassURL);
580 if (statusCode === 304) {
581 this._pollCallback(cssURL, sassURL);
584 var lastModified = this._checkLastModified(headers, cssURL);
586 this._stopPolling(cssURL, sassURL);
589 if (lastModified.getTime() < data.sassTimestamp.getTime()) {
590 this._pollCallback(cssURL, sassURL);
593 if (this._callback(cssUISourceCode, content))
594 this._stopPolling(cssURL, sassURL);
599 * @param {!WebInspector.UISourceCode} cssUISourceCode
600 * @param {string} sassURL
602 _reloadCSSFromFileSystem: function(cssUISourceCode, sassURL)
604 cssUISourceCode.requestMetadata(metadataCallback.bind(this));
607 * @param {?Date} timestamp
608 * @this {WebInspector.SASSSourceMapping.PollManager}
610 function metadataCallback(timestamp)
612 var cssURL = this._networkMapping.networkURL(cssUISourceCode);
614 this._pollCallback(cssURL, sassURL);
617 var cssTimestamp = timestamp.getTime();
618 var pollData = this._pollDataForSASSURL[sassURL];
620 this._stopPolling(cssURL, sassURL);
624 if (cssTimestamp < pollData.sassTimestamp.getTime()) {
625 this._pollCallback(cssURL, sassURL);
629 cssUISourceCode.requestOriginalContent(contentCallback.bind(this));
632 * @param {?string} content
633 * @this {WebInspector.SASSSourceMapping.PollManager}
635 function contentCallback(content)
637 // Empty string is a valid value, null means error.
638 if (content === null)
640 if (this._callback(cssUISourceCode, content))
641 this._stopPolling(cssURL, sassURL);