1 /* 2 * Copyright (C) 2011 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 */ 30 31 WebInspector.SourceFile = function(id, script, contentChangedDelegate) 32 { 33 this._scripts = [script]; 34 this._contentChangedDelegate = contentChangedDelegate; 35 if (script.sourceURL) 36 this._resource = WebInspector.networkManager.inflightResourceForURL(script.sourceURL) || WebInspector.resourceForURL(script.sourceURL); 37 this._requestContentCallbacks = []; 38 39 this.id = id; 40 this.url = script.sourceURL; 41 this.isContentScript = script.isContentScript; 42 this.messages = []; 43 this.breakpoints = {}; 44 45 if (this._hasPendingResource()) 46 this._resource.addEventListener("finished", this.reload.bind(this)); 47 } 48 49 WebInspector.SourceFile.prototype = { 50 addScript: function(script) 51 { 52 this._scripts.push(script); 53 }, 54 55 requestContent: function(callback) 56 { 57 if (this._contentLoaded) { 58 callback(this._mimeType, this._content); 59 return; 60 } 61 62 this._requestContentCallbacks.push(callback); 63 this._requestContent(); 64 }, 65 66 get content() 67 { 68 return this._content; 69 }, 70 71 set content(content) 72 { 73 // FIXME: move live edit implementation to SourceFile and remove this setter. 74 this._content = content; 75 }, 76 77 requestSourceMapping: function(callback) 78 { 79 if (!this._mapping) 80 this._mapping = new WebInspector.SourceMapping(this._scripts); 81 callback(this._mapping); 82 }, 83 84 forceLoadContent: function(script) 85 { 86 if (!this._hasPendingResource()) 87 return; 88 89 if (!this._concatenatedScripts) 90 this._concatenatedScripts = {}; 91 if (this._concatenatedScripts[script.sourceID]) 92 return; 93 for (var i = 0; i < this._scripts.length; ++i) 94 this._concatenatedScripts[this._scripts[i].sourceID] = true; 95 96 this.reload(); 97 98 if (!this._contentRequested) { 99 this._contentRequested = true; 100 this._loadAndConcatenateScriptsContent(); 101 } 102 }, 103 104 reload: function() 105 { 106 if (this._contentLoaded) { 107 this._contentLoaded = false; 108 this._contentChangedDelegate(); 109 } else if (this._contentRequested) 110 this._reloadContent = true; 111 else if (this._requestContentCallbacks.length) 112 this._requestContent(); 113 }, 114 115 _requestContent: function() 116 { 117 if (this._contentRequested) 118 return; 119 120 this._contentRequested = true; 121 if (this._resource && this._resource.finished) 122 this._loadResourceContent(this._resource); 123 else if (!this._resource) 124 this._loadScriptContent(); 125 else if (this._concatenatedScripts) 126 this._loadAndConcatenateScriptsContent(); 127 else 128 this._contentRequested = false; 129 }, 130 131 _loadResourceContent: function(resource) 132 { 133 function didRequestContent(text) 134 { 135 if (!text) { 136 this._loadAndConcatenateScriptsContent(); 137 return; 138 } 139 140 if (resource.type === WebInspector.Resource.Type.Script) 141 this._didRequestContent("text/javascript", text); 142 else { 143 // WebKit html lexer normalizes line endings and scripts are passed to VM with "\n" line endings. 144 // However, resource content has original line endings, so we have to normalize line endings here. 145 this._didRequestContent("text/html", text.replace(/\r\n/g, "\n")); 146 } 147 } 148 resource.requestContent(didRequestContent.bind(this)); 149 }, 150 151 _loadScriptContent: function() 152 { 153 this._scripts[0].requestSource(this._didRequestContent.bind(this, "text/javascript")); 154 }, 155 156 _loadAndConcatenateScriptsContent: function() 157 { 158 var scripts = this._scripts.slice(); 159 scripts.sort(function(x, y) { return x.lineOffset - y.lineOffset || x.columnOffset - y.columnOffset; }); 160 var sources = []; 161 function didRequestSource(source) 162 { 163 sources.push(source); 164 if (sources.length < scripts.length) 165 return; 166 if (scripts.length === 1 && !scripts[0].lineOffset && !scripts[0].columnOffset) 167 this._didRequestContent("text/javascript", source); 168 else 169 this._concatenateScriptsContent(scripts, sources); 170 } 171 for (var i = 0; i < scripts.length; ++i) 172 scripts[i].requestSource(didRequestSource.bind(this)); 173 }, 174 175 _concatenateScriptsContent: function(scripts, sources) 176 { 177 var content = ""; 178 var lineNumber = 0; 179 var columnNumber = 0; 180 var scriptRanges = []; 181 function appendChunk(chunk, script) 182 { 183 var start = { lineNumber: lineNumber, columnNumber: columnNumber }; 184 content += chunk; 185 var lineEndings = chunk.lineEndings(); 186 var lineCount = lineEndings.length; 187 if (lineCount === 1) 188 columnNumber += chunk.length; 189 else { 190 lineNumber += lineCount - 1; 191 columnNumber = lineEndings[lineCount - 1] - lineEndings[lineCount - 2] - 1; 192 } 193 var end = { lineNumber: lineNumber, columnNumber: columnNumber }; 194 if (script) 195 scriptRanges.push({ start: start, end: end, sourceID: script.sourceID }); 196 } 197 198 var scriptOpenTag = "<script>"; 199 var scriptCloseTag = "</script>"; 200 for (var i = 0; i < scripts.length; ++i) { 201 // Fill the gap with whitespace characters. 202 while (lineNumber < scripts[i].lineOffset) 203 appendChunk("\n"); 204 while (columnNumber < scripts[i].columnOffset - scriptOpenTag.length) 205 appendChunk(" "); 206 207 // Add script tag. 208 appendChunk(scriptOpenTag); 209 appendChunk(sources[i], scripts[i]); 210 appendChunk(scriptCloseTag); 211 } 212 213 this._didRequestContent("text/html", content); 214 }, 215 216 _didRequestContent: function(mimeType, content) 217 { 218 this._contentLoaded = true; 219 this._contentRequested = false; 220 this._mimeType = mimeType; 221 this._content = content; 222 223 for (var i = 0; i < this._requestContentCallbacks.length; ++i) 224 this._requestContentCallbacks[i](mimeType, content); 225 this._requestContentCallbacks = []; 226 227 if (this._reloadContent) 228 this.reload(); 229 }, 230 231 _hasPendingResource: function() 232 { 233 return this._resource && !this._resource.finished; 234 } 235 } 236 237 WebInspector.FormattedSourceFile = function(sourceFileId, script, contentChangedDelegate, formatter) 238 { 239 WebInspector.SourceFile.call(this, sourceFileId, script, contentChangedDelegate); 240 this._formatter = formatter; 241 } 242 243 WebInspector.FormattedSourceFile.prototype = { 244 requestSourceMapping: function(callback) 245 { 246 function didRequestContent() 247 { 248 callback(this._mapping); 249 } 250 this.requestContent(didRequestContent.bind(this)); 251 }, 252 253 _didRequestContent: function(mimeType, text) 254 { 255 function didFormatContent(formattedText, mapping) 256 { 257 this._mapping = new WebInspector.FormattedSourceMapping(this._scripts, text, formattedText, mapping); 258 WebInspector.SourceFile.prototype._didRequestContent.call(this, mimeType, formattedText); 259 } 260 this._formatter.formatContent(text, this._scripts, didFormatContent.bind(this)); 261 } 262 } 263 264 WebInspector.FormattedSourceFile.prototype.__proto__ = WebInspector.SourceFile.prototype; 265 266 WebInspector.SourceMapping = function(scripts) 267 { 268 this._sortedScripts = scripts.slice(); 269 this._sortedScripts.sort(function(x, y) { return x.lineOffset - y.lineOffset || x.columnOffset - y.columnOffset; }); 270 } 271 272 WebInspector.SourceMapping.prototype = { 273 scriptLocationToSourceLine: function(location) 274 { 275 return location.lineNumber; 276 }, 277 278 sourceLineToScriptLocation: function(lineNumber) 279 { 280 return this._sourceLocationToScriptLocation(lineNumber, 0); 281 }, 282 283 _sourceLocationToScriptLocation: function(lineNumber, columnNumber) 284 { 285 var closestScript = this._sortedScripts[0]; 286 for (var i = 1; i < this._sortedScripts.length; ++i) { 287 script = this._sortedScripts[i]; 288 if (script.lineOffset > lineNumber || (script.lineOffset === lineNumber && script.columnOffset > columnNumber)) 289 break; 290 closestScript = script; 291 } 292 return { sourceID: closestScript.sourceID, lineNumber: lineNumber, columnNumber: columnNumber }; 293 } 294 } 295 296 WebInspector.FormattedSourceMapping = function(scripts, originalText, formattedText, mapping) 297 { 298 WebInspector.SourceMapping.call(this, scripts); 299 this._originalLineEndings = originalText.lineEndings(); 300 this._formattedLineEndings = formattedText.lineEndings(); 301 this._mapping = mapping; 302 } 303 304 WebInspector.FormattedSourceMapping.prototype = { 305 scriptLocationToSourceLine: function(location) 306 { 307 var originalPosition = WebInspector.ScriptFormatter.locationToPosition(this._originalLineEndings, location); 308 var index = this._mapping.original.upperBound(originalPosition - 1); 309 var formattedPosition = this._mapping.formatted[index]; 310 return WebInspector.ScriptFormatter.positionToLocation(this._formattedLineEndings, formattedPosition).lineNumber; 311 }, 312 313 sourceLineToScriptLocation: function(lineNumber) 314 { 315 var formattedPosition = WebInspector.ScriptFormatter.lineToPosition(this._formattedLineEndings, lineNumber); 316 var index = this._mapping.formatted.upperBound(formattedPosition - 1); 317 var originalPosition = this._mapping.original[index]; 318 var originalLocation = WebInspector.ScriptFormatter.positionToLocation(this._originalLineEndings, originalPosition); 319 return WebInspector.SourceMapping.prototype._sourceLocationToScriptLocation.call(this, originalLocation.lineNumber, originalLocation.columnNumber); 320 } 321 } 322 323 WebInspector.FormattedSourceMapping.prototype.__proto__ = WebInspector.SourceMapping.prototype; 324