Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / third_party / WebKit / Source / devtools / front_end / bindings / SASSSourceMapping.js
blob370fe3bdc50d1608ef166eed7b763a8964d963a0
1 /*
2  * Copyright (C) 2012 Google Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions are
6  * met:
7  *
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
13  * distribution.
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.
17  *
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.
29  */
31 /**
32  * @constructor
33  * @implements {WebInspector.CSSSourceMapping}
34  * @param {!WebInspector.CSSStyleModel} cssModel
35  * @param {!WebInspector.Workspace} workspace
36  * @param {!WebInspector.NetworkMapping} networkMapping
37  * @param {!WebInspector.NetworkProject} networkProject
38  */
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));
46     this._reset();
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 = {
57     /**
58      * @param {!WebInspector.Event} event
59      */
60     _styleSheetChanged: function(event)
61     {
62         var id = /** @type {!CSSAgent.StyleSheetId} */ (event.data.styleSheetId);
63         if (this._addingRevisionCounter) {
64             --this._addingRevisionCounter;
65             return;
66         }
67         var header = this._cssModel.styleSheetHeaderForId(id);
68         if (!header)
69             return;
71         this.removeHeader(header);
72     },
74     /**
75      * @param {!WebInspector.Event} event
76      */
77     _toggleSourceMapSupport: function(event)
78     {
79         var enabled = /** @type {boolean} */ (event.data);
80         var headers = this._cssModel.styleSheetHeaders();
81         for (var i = 0; i < headers.length; ++i) {
82             if (enabled)
83                 this.addHeader(headers[i]);
84             else
85                 this.removeHeader(headers[i]);
86         }
87     },
89     /**
90      * @param {!WebInspector.Event} event
91      */
92     _fileSaveFinished: function(event)
93     {
94         var sassURL = /** @type {string} */ (event.data);
95         var cssURLs = this._sassURLToCSSURLs.get(sassURL).valuesArray();
96         this._pollManager.sassFileChanged(sassURL, cssURLs, false);
97     },
99     /**
100      * @param {!WebInspector.UISourceCode} cssUISourceCode
101      * @param {string} content
102      * @return {boolean}
103      */
104     _updateCSSRevision: function(cssUISourceCode, content)
105     {
106         ++this._addingRevisionCounter;
107         cssUISourceCode.addRevision(content);
108         var cssURL = this._networkMapping.networkURL(cssUISourceCode);
109         var completeSourceMapURL = this._completeSourceMapURLForCSSURL[cssURL];
110         if (!completeSourceMapURL)
111             return false;
112         var ids = this._cssModel.styleSheetIdsForURL(cssURL);
113         if (!ids)
114             return false;
115         var headers = [];
116         for (var i = 0; i < ids.length; ++i)
117             headers.push(this._cssModel.styleSheetHeaderForId(ids[i]));
118         this._loadSourceMapAndBindUISourceCode(headers, true, completeSourceMapURL);
119         return true;
120     },
122     /**
123      * @param {!WebInspector.CSSStyleSheetHeader} header
124      */
125     addHeader: function(header)
126     {
127         if (!header.sourceMapURL || !header.sourceURL || !WebInspector.moduleSetting("cssSourceMapsEnabled").get())
128             return;
129         var completeSourceMapURL = WebInspector.ParsedURL.completeURL(header.sourceURL, header.sourceMapURL);
130         if (!completeSourceMapURL)
131             return;
132         this._completeSourceMapURLForCSSURL[header.sourceURL] = completeSourceMapURL;
133         this._loadSourceMapAndBindUISourceCode([header], false, completeSourceMapURL);
134     },
136     /**
137      * @param {!WebInspector.CSSStyleSheetHeader} header
138      */
139     removeHeader: function(header)
140     {
141         var sourceURL = header.sourceURL;
142         if (!sourceURL || !header.sourceMapURL || !this._completeSourceMapURLForCSSURL[sourceURL])
143             return;
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);
155     },
157     /**
158      * @param {!Array.<!WebInspector.CSSStyleSheetHeader>} headersWithSameSourceURL
159      * @param {boolean} forceRebind
160      * @param {string} completeSourceMapURL
161      */
162     _loadSourceMapAndBindUISourceCode: function(headersWithSameSourceURL, forceRebind, completeSourceMapURL)
163     {
164         console.assert(headersWithSameSourceURL.length);
165         var sourceURL = headersWithSameSourceURL[0].sourceURL;
166         this._loadSourceMapForStyleSheet(completeSourceMapURL, sourceURL, forceRebind, sourceMapLoaded.bind(this));
168         /**
169          * @param {?WebInspector.SourceMap} sourceMap
170          * @this {WebInspector.SASSSourceMapping}
171          */
172         function sourceMapLoaded(sourceMap)
173         {
174             if (!sourceMap)
175                 return;
177             this._sourceMapByStyleSheetURL[sourceURL] = sourceMap;
178             for (var i = 0; i < headersWithSameSourceURL.length; ++i) {
179                 if (forceRebind)
180                     WebInspector.cssWorkspaceBinding.updateLocations(headersWithSameSourceURL[i]);
181                 else
182                     this._bindUISourceCode(headersWithSameSourceURL[i], sourceMap);
183             }
184         }
185     },
187     /**
188      * @param {string} completeSourceMapURL
189      * @param {string} completeStyleSheetURL
190      * @param {boolean} forceReload
191      * @param {function(?WebInspector.SourceMap)} callback
192      */
193     _loadSourceMapForStyleSheet: function(completeSourceMapURL, completeStyleSheetURL, forceReload, callback)
194     {
195         var sourceMap = this._sourceMapByURL[completeSourceMapURL];
196         if (sourceMap && !forceReload) {
197             callback(sourceMap);
198             return;
199         }
201         var pendingCallbacks = this._pendingSourceMapLoadingCallbacks[completeSourceMapURL];
202         if (pendingCallbacks) {
203             pendingCallbacks.push(callback);
204             return;
205         }
207         pendingCallbacks = [callback];
208         this._pendingSourceMapLoadingCallbacks[completeSourceMapURL] = pendingCallbacks;
210         WebInspector.SourceMap.load(completeSourceMapURL, completeStyleSheetURL, sourceMapLoaded.bind(this));
212         /**
213          * @param {?WebInspector.SourceMap} sourceMap
214          * @this {WebInspector.SASSSourceMapping}
215          */
216         function sourceMapLoaded(sourceMap)
217         {
218             var callbacks = this._pendingSourceMapLoadingCallbacks[completeSourceMapURL];
219             delete this._pendingSourceMapLoadingCallbacks[completeSourceMapURL];
220             if (!callbacks)
221                 return;
222             if (sourceMap)
223                 this._sourceMapByURL[completeSourceMapURL] = sourceMap;
224             else
225                 delete this._sourceMapByURL[completeSourceMapURL];
226             for (var i = 0; i < callbacks.length; ++i)
227                 callbacks[i](sourceMap);
228         }
229     },
231     /**
232      * @param {!WebInspector.CSSStyleSheetHeader} header
233      * @param {!WebInspector.SourceMap} sourceMap
234      */
235     _bindUISourceCode: function(header, sourceMap)
236     {
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);
246             }
247         }
248     },
250     /**
251      * @override
252      * @param {!WebInspector.CSSLocation} rawLocation
253      * @return {?WebInspector.UILocation}
254      */
255     rawLocationToUILocation: function(rawLocation)
256     {
257         var sourceMap = this._sourceMapByStyleSheetURL[rawLocation.url];
258         if (!sourceMap)
259             return null;
260         var entry = sourceMap.findEntry(rawLocation.lineNumber, rawLocation.columnNumber);
261         if (!entry || !entry.sourceURL)
262             return null;
263         var uiSourceCode = this._networkMapping.uiSourceCodeForURL(entry.sourceURL, rawLocation.target());
264         if (!uiSourceCode)
265             return null;
266         return uiSourceCode.uiLocation(entry.sourceLineNumber, entry.sourceColumnNumber);
267     },
269     /**
270      * @override
271      * @param {!WebInspector.UISourceCode} uiSourceCode
272      * @param {number} lineNumber
273      * @param {number} columnNumber
274      * @return {?WebInspector.CSSLocation}
275      */
276     uiLocationToRawLocation: function(uiSourceCode, lineNumber, columnNumber)
277     {
278         return null;
279     },
281     /**
282      * @override
283      * @return {boolean}
284      */
285     isIdentity: function()
286     {
287         return false;
288     },
290     /**
291      * @override
292      * @param {!WebInspector.UISourceCode} uiSourceCode
293      * @param {number} lineNumber
294      * @return {boolean}
295      */
296     uiLineHasMapping: function(uiSourceCode, lineNumber)
297     {
298         return true;
299     },
301     /**
302      * @return {!WebInspector.Target}
303      */
304     target: function()
305     {
306         return this._cssModel.target();
307     },
309     /**
310      * @param {!WebInspector.Event} event
311      */
312     _uiSourceCodeAdded: function(event)
313     {
314         var uiSourceCode = /** @type {!WebInspector.UISourceCode} */ (event.data);
315         var networkURL = this._networkMapping.networkURL(uiSourceCode);
316         var cssURLs = this._sassURLToCSSURLs.get(networkURL).valuesArray();
317         if (!cssURLs)
318             return;
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));
325             }
326         }
327     },
329     /**
330      * @param {!WebInspector.Event} event
331      */
332     _uiSourceCodeContentCommitted: function(event)
333     {
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);
339         }
340     },
342     _reset: function()
343     {
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();
354     }
358  * @constructor
359  * @param {!WebInspector.CSSStyleModel} cssModel
360  * @param {!WebInspector.NetworkMapping} networkMapping
361  * @param {function(!WebInspector.UISourceCode, string):boolean} callback
362  */
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;
370     this.reset();
373 WebInspector.SASSSourceMapping.PollManager.prototype = {
374     reset: function()
375     {
376         /** @type {!Object.<string, !{deadlineMs: number, dataByURL: !Object.<string, !{timer: number, previousPoll: number}>}>} */
377         this._pollDataForSASSURL = {};
378     },
380     /**
381      * @param {string} headerName
382      * @param {!Object.<string, string>} headers
383      * @return {?string}
384      */
385     _headerValue: function(headerName, headers)
386     {
387         headerName = headerName.toLowerCase();
388         var value = null;
389         for (var name in headers) {
390             if (name.toLowerCase() === headerName) {
391                 value = headers[name];
392                 break;
393             }
394         }
395         return value;
396     },
398     /**
399      * @param {!Object.<string, string>} headers
400      * @return {?Date}
401      */
402     _lastModified: function(headers)
403     {
404         var lastModifiedHeader = this._headerValue("last-modified", headers);
405         if (!lastModifiedHeader)
406             return null;
407         var lastModified = new Date(lastModifiedHeader);
408         if (isNaN(lastModified.getTime()))
409             return null;
410         return lastModified;
411     },
413     /**
414      * @param {!Object.<string, string>} headers
415      * @param {string} url
416      * @return {?Date}
417      */
418     _checkLastModified: function(headers, url)
419     {
420         var lastModified = this._lastModified(headers);
421         if (lastModified)
422             return lastModified;
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);
427         return null;
428     },
430     /**
431      * @param {string} sassURL
432      * @param {!Array.<string>} cssURLs
433      * @param {boolean} wasLoadedFromFileSystem
434      */
435     sassFileChanged: function(sassURL, cssURLs, wasLoadedFromFileSystem)
436     {
437         if (!cssURLs)
438             return;
439         if (!WebInspector.moduleSetting("cssReloadEnabled").get())
440             return;
442         var sassFile = this._networkMapping.uiSourceCodeForURL(sassURL, this._cssModel.target());
443         console.assert(sassFile);
444         if (wasLoadedFromFileSystem)
445             sassFile.requestMetadata(metadataReceived.bind(this));
446         else
447             WebInspector.ResourceLoader.loadUsingTargetUA(sassURL, null, sassLoadedViaNetwork.bind(this));
449         /**
450          * @param {number} statusCode
451          * @param {!Object.<string, string>} headers
452          * @param {string} content
453          * @this {WebInspector.SASSSourceMapping.PollManager}
454          */
455         function sassLoadedViaNetwork(statusCode, headers, content)
456         {
457             if (statusCode >= 400) {
458                 console.error("Could not load content for " + sassURL + " : " + "HTTP status code: " + statusCode);
459                 return;
460             }
461             var lastModified = this._checkLastModified(headers, sassURL);
462             if (!lastModified)
463                 return;
464             metadataReceived.call(this, lastModified);
465         }
467         /**
468          * @param {?Date} timestamp
469          * @this {WebInspector.SASSSourceMapping.PollManager}
470          */
471         function metadataReceived(timestamp)
472         {
473             if (!timestamp)
474                 return;
476             var now = Date.now();
477             var deadlineMs = now + this.pollPeriodMs;
478             var pollData = this._pollDataForSASSURL[sassURL];
479             if (pollData) {
480                 var dataByURL = pollData.dataByURL;
481                 for (var url in dataByURL)
482                     clearTimeout(dataByURL[url].timer);
483             }
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);
489             }
490         }
491     },
493     /**
494      * @param {string} cssURL
495      * @param {string} sassURL
496      */
497     _pollCallback: function(cssURL, sassURL)
498     {
499         var now;
500         var pollData = this._pollDataForSASSURL[sassURL];
501         if (!pollData)
502             return;
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);
507             return;
508         }
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);
513     },
515     /**
516      * @param {string} cssURL
517      * @param {string} sassURL
518      */
519     _stopPolling: function(cssURL, sassURL)
520     {
521         var pollData = this._pollDataForSASSURL[sassURL];
522         if (!pollData)
523             return;
524         delete pollData.dataByURL[cssURL];
525         if (!Object.keys(pollData.dataByURL).length)
526             delete this._pollDataForSASSURL[sassURL];
527     },
529     /**
530      * @param {string} cssURL
531      * @param {string} sassURL
532      */
533     _reloadCSS: function(cssURL, sassURL)
534     {
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)
539             return;
540         }
542         if (this._networkMapping.hasMappingForURL(sassURL))
543             this._reloadCSSFromFileSystem(cssUISourceCode, sassURL);
544         else
545             this._reloadCSSFromNetwork(cssUISourceCode, sassURL);
546     },
548     /**
549      * @param {!WebInspector.UISourceCode} cssUISourceCode
550      * @param {string} sassURL
551      */
552     _reloadCSSFromNetwork: function(cssUISourceCode, sassURL)
553     {
554         var cssURL = this._networkMapping.networkURL(cssUISourceCode);
555         var data = this._pollDataForSASSURL[sassURL];
556         if (!data) {
557             this._stopPolling(cssURL, sassURL);
558             return;
559         }
560         var headers = { "if-modified-since": new Date(data.sassTimestamp.getTime() - 1000).toUTCString() };
561         WebInspector.ResourceLoader.loadUsingTargetUA(cssURL, headers, contentLoaded.bind(this));
563         /**
564          * @param {number} statusCode
565          * @param {!Object.<string, string>} headers
566          * @param {string} content
567          * @this {WebInspector.SASSSourceMapping.PollManager}
568          */
569         function contentLoaded(statusCode, headers, content)
570         {
571             if (statusCode >= 400) {
572                 console.error("Could not load content for " + cssURL + " : " + "HTTP status code: " + statusCode);
573                 this._stopPolling(cssURL, sassURL);
574                 return;
575             }
576             if (!this._pollDataForSASSURL[sassURL]) {
577                 this._stopPolling(cssURL, sassURL);
578                 return;
579             }
580             if (statusCode === 304) {
581                 this._pollCallback(cssURL, sassURL);
582                 return;
583             }
584             var lastModified = this._checkLastModified(headers, cssURL);
585             if (!lastModified) {
586                 this._stopPolling(cssURL, sassURL);
587                 return;
588             }
589             if (lastModified.getTime() < data.sassTimestamp.getTime()) {
590                 this._pollCallback(cssURL, sassURL);
591                 return;
592             }
593             if (this._callback(cssUISourceCode, content))
594                 this._stopPolling(cssURL, sassURL);
595         }
596     },
598     /**
599      * @param {!WebInspector.UISourceCode} cssUISourceCode
600      * @param {string} sassURL
601      */
602     _reloadCSSFromFileSystem: function(cssUISourceCode, sassURL)
603     {
604         cssUISourceCode.requestMetadata(metadataCallback.bind(this));
606         /**
607          * @param {?Date} timestamp
608          * @this {WebInspector.SASSSourceMapping.PollManager}
609          */
610         function metadataCallback(timestamp)
611         {
612             var cssURL = this._networkMapping.networkURL(cssUISourceCode);
613             if (!timestamp) {
614                 this._pollCallback(cssURL, sassURL);
615                 return;
616             }
617             var cssTimestamp = timestamp.getTime();
618             var pollData = this._pollDataForSASSURL[sassURL];
619             if (!pollData) {
620                 this._stopPolling(cssURL, sassURL);
621                 return;
622             }
624             if (cssTimestamp < pollData.sassTimestamp.getTime()) {
625                 this._pollCallback(cssURL, sassURL);
626                 return;
627             }
629             cssUISourceCode.requestOriginalContent(contentCallback.bind(this));
631             /**
632              * @param {?string} content
633              * @this {WebInspector.SASSSourceMapping.PollManager}
634              */
635             function contentCallback(content)
636             {
637                 // Empty string is a valid value, null means error.
638                 if (content === null)
639                     return;
640                 if (this._callback(cssUISourceCode, content))
641                     this._stopPolling(cssURL, sassURL);
642             }
643         }
644     }