OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. |
| 4 |
| 5 /// Helper for creating HTML visualization of the source map information |
| 6 /// generated by a [SourceMapProcessor]. |
| 7 |
| 8 library sourcemap.html.helper; |
| 9 |
| 10 import 'dart:convert'; |
| 11 |
| 12 import 'package:compiler/src/io/source_file.dart'; |
| 13 import 'package:compiler/src/io/source_information.dart'; |
| 14 import 'package:compiler/src/js/js.dart' as js; |
| 15 |
| 16 import 'colors.dart'; |
| 17 import 'sourcemap_helper.dart'; |
| 18 import 'sourcemap_html_templates.dart'; |
| 19 |
| 20 /// Returns the [index]th color for visualization. |
| 21 String toColor(int index) { |
| 22 int hueCount = 24; |
| 23 double h = 360.0 * (index % hueCount) / hueCount; |
| 24 double v = 1.0; |
| 25 double s = 0.5; |
| 26 HSV hsv = new HSV(h, s, v); |
| 27 RGB rgb = HSV.toRGB(hsv); |
| 28 return rgb.toHex; |
| 29 } |
| 30 |
| 31 /// Return the html for the [index] line number. |
| 32 String lineNumber(int index) { |
| 33 return '<span class="lineNumber">${index + 1} </span>'; |
| 34 } |
| 35 |
| 36 /// Return the html escaped [text]. |
| 37 String escape(String text) { |
| 38 return const HtmlEscape().convert(text); |
| 39 } |
| 40 |
| 41 /// Information needed to generate HTML for a single [SourceMapInfo]. |
| 42 class SourceMapHtmlInfo { |
| 43 final SourceMapInfo sourceMapInfo; |
| 44 final CodeProcessor codeProcessor; |
| 45 final SourceLocationCollection sourceLocationCollection; |
| 46 |
| 47 SourceMapHtmlInfo(this.sourceMapInfo, |
| 48 this.codeProcessor, |
| 49 this.sourceLocationCollection); |
| 50 } |
| 51 |
| 52 /// A collection of source locations. |
| 53 /// |
| 54 /// Used to index source locations for visualization and linking. |
| 55 class SourceLocationCollection { |
| 56 List<SourceLocation> sourceLocations = []; |
| 57 Map<SourceLocation, int> sourceLocationIndexMap; |
| 58 |
| 59 SourceLocationCollection([SourceLocationCollection parent]) |
| 60 : sourceLocationIndexMap = |
| 61 parent == null ? {} : parent.sourceLocationIndexMap; |
| 62 |
| 63 int registerSourceLocation(SourceLocation sourceLocation) { |
| 64 return sourceLocationIndexMap.putIfAbsent(sourceLocation, () { |
| 65 sourceLocations.add(sourceLocation); |
| 66 return sourceLocationIndexMap.length; |
| 67 }); |
| 68 } |
| 69 |
| 70 int getIndex(SourceLocation sourceLocation) { |
| 71 return sourceLocationIndexMap[sourceLocation]; |
| 72 } |
| 73 } |
| 74 |
| 75 /// Processor that computes the HTML representation of a block of JavaScript |
| 76 /// code and collects the source locations mapped in the code. |
| 77 class CodeProcessor { |
| 78 int lineIndex = 0; |
| 79 final String onclick; |
| 80 int currentJsSourceOffset = 0; |
| 81 final SourceLocationCollection collection; |
| 82 final Map<int, List<SourceLocation>> codeLocations = {}; |
| 83 |
| 84 CodeProcessor(this.onclick, this.collection); |
| 85 |
| 86 void addSourceLocation(int targetOffset, SourceLocation sourceLocation) { |
| 87 codeLocations.putIfAbsent(targetOffset, () => []).add(sourceLocation); |
| 88 collection.registerSourceLocation(sourceLocation); |
| 89 } |
| 90 |
| 91 String convertToHtml(String text) { |
| 92 StringBuffer htmlBuffer = new StringBuffer(); |
| 93 int offset = 0; |
| 94 int lineIndex = 0; |
| 95 bool pendingSourceLocationsEnd = false; |
| 96 htmlBuffer.write(lineNumber(lineIndex)); |
| 97 SourceLocation currentLocation; |
| 98 |
| 99 void endCurrentLocation() { |
| 100 if (currentLocation != null) { |
| 101 htmlBuffer.write('</a>'); |
| 102 } |
| 103 currentLocation = null; |
| 104 } |
| 105 |
| 106 void addSubstring(int until) { |
| 107 if (until <= offset) return; |
| 108 |
| 109 String substring = text.substring(offset, until); |
| 110 offset = until; |
| 111 bool first = true; |
| 112 for (String line in substring.split('\n')) { |
| 113 if (!first) { |
| 114 endCurrentLocation(); |
| 115 htmlBuffer.write('\n'); |
| 116 lineIndex++; |
| 117 htmlBuffer.write(lineNumber(lineIndex)); |
| 118 } |
| 119 htmlBuffer.write(escape(line)); |
| 120 first = false; |
| 121 } |
| 122 } |
| 123 |
| 124 void insertSourceLocations(List<SourceLocation> lastSourceLocations) { |
| 125 endCurrentLocation(); |
| 126 |
| 127 String color; |
| 128 int index; |
| 129 String title; |
| 130 if (lastSourceLocations.length == 1) { |
| 131 SourceLocation sourceLocation = lastSourceLocations.single; |
| 132 if (sourceLocation != null) { |
| 133 index = collection.getIndex(sourceLocation); |
| 134 color = "background:#${toColor(index)};"; |
| 135 title = sourceLocation.shortText; |
| 136 currentLocation = sourceLocation; |
| 137 } |
| 138 } else { |
| 139 |
| 140 index = collection.getIndex(lastSourceLocations.first); |
| 141 StringBuffer sb = new StringBuffer(); |
| 142 double delta = 100.0 / (lastSourceLocations.length); |
| 143 double position = 0.0; |
| 144 |
| 145 void addColor(String color) { |
| 146 sb.write(', ${color} ${position.toInt()}%'); |
| 147 position += delta; |
| 148 sb.write(', ${color} ${position.toInt()}%'); |
| 149 } |
| 150 |
| 151 for (SourceLocation sourceLocation in lastSourceLocations) { |
| 152 if (sourceLocation == null) continue; |
| 153 int colorIndex = collection.getIndex(sourceLocation); |
| 154 addColor('#${toColor(colorIndex)}'); |
| 155 currentLocation = sourceLocation; |
| 156 } |
| 157 color = 'background: linear-gradient(to right${sb}); ' |
| 158 'background-size: 10px 10px;'; |
| 159 title = lastSourceLocations.map((l) => l.shortText).join(','); |
| 160 } |
| 161 if (index != null) { |
| 162 Set<int> indices = |
| 163 lastSourceLocations.map((l) => collection.getIndex(l)).toSet(); |
| 164 String onmouseover = indices.map((i) => '\'$i\'').join(','); |
| 165 htmlBuffer.write( |
| 166 '<a name="js$index" href="#${index}" style="$color" title="$title" ' |
| 167 'onclick="${onclick}" onmouseover="highlight([${onmouseover}]);"' |
| 168 'onmouseout="highlight([]);">'); |
| 169 pendingSourceLocationsEnd = true; |
| 170 } |
| 171 if (lastSourceLocations.last == null) { |
| 172 endCurrentLocation(); |
| 173 } |
| 174 } |
| 175 |
| 176 for (int targetOffset in codeLocations.keys.toList()..sort()) { |
| 177 List<SourceLocation> sourceLocations = codeLocations[targetOffset]; |
| 178 addSubstring(targetOffset); |
| 179 insertSourceLocations(sourceLocations); |
| 180 } |
| 181 |
| 182 addSubstring(text.length); |
| 183 endCurrentLocation(); |
| 184 return htmlBuffer.toString(); |
| 185 } |
| 186 } |
| 187 |
| 188 /// Computes the HTML representation for a collection of JavaScript code blocks. |
| 189 String computeJsHtml(Iterable<SourceMapHtmlInfo> infoList) { |
| 190 |
| 191 StringBuffer jsCodeBuffer = new StringBuffer(); |
| 192 for (SourceMapHtmlInfo info in infoList) { |
| 193 String name = info.sourceMapInfo.name; |
| 194 String html = info.codeProcessor.convertToHtml(info.sourceMapInfo.code); |
| 195 String onclick = 'show(\'$name\');'; |
| 196 jsCodeBuffer.write( |
| 197 '<h3 onclick="$onclick">JS code for: ${escape(name)}</h3>\n'); |
| 198 jsCodeBuffer.write(''' |
| 199 <pre> |
| 200 $html |
| 201 </pre> |
| 202 '''); |
| 203 } |
| 204 return jsCodeBuffer.toString(); |
| 205 } |
| 206 |
| 207 /// Computes the HTML representation of the source mapping information for a |
| 208 /// collection of JavaScript code blocks. |
| 209 String computeJsTraceHtml(Iterable<SourceMapHtmlInfo> infoList) { |
| 210 StringBuffer jsTraceBuffer = new StringBuffer(); |
| 211 for (SourceMapHtmlInfo info in infoList) { |
| 212 String name = info.sourceMapInfo.name; |
| 213 String jsTrace = computeJsTraceHtmlPart( |
| 214 info.sourceMapInfo.codePoints, info.sourceLocationCollection); |
| 215 jsTraceBuffer.write(''' |
| 216 <div name="$name" class="js-trace-buffer" style="display:none;"> |
| 217 <h3>Trace for: ${escape(name)}</h3> |
| 218 $jsTrace |
| 219 </div> |
| 220 '''); |
| 221 } |
| 222 return jsTraceBuffer.toString(); |
| 223 } |
| 224 |
| 225 /// Computes the HTML information for the [info]. |
| 226 SourceMapHtmlInfo createHtmlInfo(SourceLocationCollection collection, |
| 227 SourceMapInfo info) { |
| 228 js.Node node = info.node; |
| 229 String code = info.code; |
| 230 String name = info.name; |
| 231 String onclick = 'show(\'$name\');'; |
| 232 SourceLocationCollection subcollection = |
| 233 new SourceLocationCollection(collection); |
| 234 CodeProcessor codeProcessor = new CodeProcessor(onclick, subcollection); |
| 235 for (js.Node node in info.nodeMap.nodes) { |
| 236 info.nodeMap[node].forEach( |
| 237 (int targetOffset, List<SourceLocation> sourceLocations) { |
| 238 for (SourceLocation sourceLocation in sourceLocations) { |
| 239 codeProcessor.addSourceLocation(targetOffset, sourceLocation); |
| 240 } |
| 241 }); |
| 242 } |
| 243 return new SourceMapHtmlInfo(info, codeProcessor, subcollection); |
| 244 } |
| 245 |
| 246 /// Outputs a HTML file in [jsMapHtmlUri] containing an interactive |
| 247 /// visualization of the source mapping information in [infoList] computed |
| 248 /// with the [sourceMapProcessor]. |
| 249 void createTraceSourceMapHtml(Uri jsMapHtmlUri, |
| 250 SourceMapProcessor sourceMapProcessor, |
| 251 Iterable<SourceMapInfo> infoList) { |
| 252 SourceFileManager sourceFileManager = sourceMapProcessor.sourceFileManager; |
| 253 SourceLocationCollection collection = new SourceLocationCollection(); |
| 254 List<SourceMapHtmlInfo> htmlInfoList = <SourceMapHtmlInfo>[]; |
| 255 for (SourceMapInfo info in infoList) { |
| 256 htmlInfoList.add(createHtmlInfo(collection, info)); |
| 257 } |
| 258 |
| 259 String jsCode = computeJsHtml(htmlInfoList); |
| 260 String dartCode = computeDartHtml(sourceFileManager, htmlInfoList); |
| 261 |
| 262 String jsTraceHtml = computeJsTraceHtml(htmlInfoList); |
| 263 outputJsDartTrace(jsMapHtmlUri, jsCode, dartCode, jsTraceHtml); |
| 264 print('Trace source map html generated: $jsMapHtmlUri'); |
| 265 } |
| 266 |
| 267 /// Computes the HTML representation for the Dart code snippets referenced in |
| 268 /// [infoList]. |
| 269 String computeDartHtml( |
| 270 SourceFileManager sourceFileManager, |
| 271 Iterable<SourceMapHtmlInfo> infoList) { |
| 272 |
| 273 StringBuffer dartCodeBuffer = new StringBuffer(); |
| 274 for (SourceMapHtmlInfo info in infoList) { |
| 275 dartCodeBuffer.write(computeDartHtmlPart(info.sourceMapInfo.name, |
| 276 sourceFileManager, info.sourceLocationCollection)); |
| 277 } |
| 278 return dartCodeBuffer.toString(); |
| 279 |
| 280 } |
| 281 |
| 282 /// Computes the HTML representation for the Dart code snippets in [collection]. |
| 283 String computeDartHtmlPart(String name, |
| 284 SourceFileManager sourceFileManager, |
| 285 SourceLocationCollection collection, |
| 286 {bool showAsBlock: false}) { |
| 287 const int windowSize = 3; |
| 288 StringBuffer dartCodeBuffer = new StringBuffer(); |
| 289 Map<Uri, Map<int, List<SourceLocation>>> sourceLocationMap = {}; |
| 290 collection.sourceLocations.forEach((SourceLocation sourceLocation) { |
| 291 Map<int, List<SourceLocation>> uriMap = |
| 292 sourceLocationMap.putIfAbsent(sourceLocation.sourceUri, () => {}); |
| 293 List<SourceLocation> lineList = |
| 294 uriMap.putIfAbsent(sourceLocation.line, () => []); |
| 295 lineList.add(sourceLocation); |
| 296 }); |
| 297 sourceLocationMap.forEach((Uri uri, Map<int, List<SourceLocation>> uriMap) { |
| 298 SourceFile sourceFile = sourceFileManager.getSourceFile(uri); |
| 299 StringBuffer codeBuffer = new StringBuffer(); |
| 300 |
| 301 int firstLineIndex; |
| 302 int lastLineIndex; |
| 303 |
| 304 void flush() { |
| 305 if (firstLineIndex != null && lastLineIndex != null) { |
| 306 dartCodeBuffer.write( |
| 307 '<h4>${uri.pathSegments.last}, ' |
| 308 '${firstLineIndex - windowSize + 1}-' |
| 309 '${lastLineIndex + windowSize + 1}' |
| 310 '</h4>\n'); |
| 311 dartCodeBuffer.write('<pre>\n'); |
| 312 for (int line = firstLineIndex - windowSize; |
| 313 line < firstLineIndex; |
| 314 line++) { |
| 315 if (line >= 0) { |
| 316 dartCodeBuffer.write(lineNumber(line)); |
| 317 dartCodeBuffer.write(sourceFile.getLineText(line)); |
| 318 } |
| 319 } |
| 320 dartCodeBuffer.write(codeBuffer); |
| 321 for (int line = lastLineIndex + 1; |
| 322 line <= lastLineIndex + windowSize; |
| 323 line++) { |
| 324 if (line < sourceFile.lines) { |
| 325 dartCodeBuffer.write(lineNumber(line)); |
| 326 dartCodeBuffer.write(sourceFile.getLineText(line)); |
| 327 } |
| 328 } |
| 329 dartCodeBuffer.write('</pre>\n'); |
| 330 firstLineIndex = null; |
| 331 lastLineIndex = null; |
| 332 } |
| 333 codeBuffer.clear(); |
| 334 } |
| 335 |
| 336 List<int> lineIndices = uriMap.keys.toList()..sort(); |
| 337 lineIndices.forEach((int lineIndex) { |
| 338 List<SourceLocation> locations = uriMap[lineIndex]; |
| 339 if (lastLineIndex != null && |
| 340 lastLineIndex + windowSize * 4 < lineIndex) { |
| 341 flush(); |
| 342 } |
| 343 if (firstLineIndex == null) { |
| 344 firstLineIndex = lineIndex; |
| 345 } else { |
| 346 for (int line = lastLineIndex + 1; line < lineIndex; line++) { |
| 347 codeBuffer.write(lineNumber(line)); |
| 348 codeBuffer.write(sourceFile.getLineText(line)); |
| 349 } |
| 350 } |
| 351 String line = sourceFile.getLineText(lineIndex); |
| 352 locations.sort((a, b) => a.offset.compareTo(b.offset)); |
| 353 for (int i = 0; i < locations.length; i++) { |
| 354 SourceLocation sourceLocation = locations[i]; |
| 355 int index = collection.getIndex(sourceLocation); |
| 356 int start = sourceLocation.column; |
| 357 int end = line.length; |
| 358 if (i + 1 < locations.length) { |
| 359 end = locations[i + 1].column; |
| 360 } |
| 361 if (i == 0) { |
| 362 codeBuffer.write(lineNumber(lineIndex)); |
| 363 codeBuffer.write(line.substring(0, start)); |
| 364 } |
| 365 codeBuffer.write( |
| 366 '<a name="${index}" style="background:#${toColor(index)};" ' |
| 367 'title="[${lineIndex + 1},${start + 1}]" ' |
| 368 'onmouseover="highlight(\'$index\');" ' |
| 369 'onmouseout="highlight();">'); |
| 370 codeBuffer.write(line.substring(start, end)); |
| 371 codeBuffer.write('</a>'); |
| 372 } |
| 373 lastLineIndex = lineIndex; |
| 374 }); |
| 375 |
| 376 flush(); |
| 377 }); |
| 378 String display = showAsBlock ? 'block' : 'none'; |
| 379 return ''' |
| 380 <div name="$name" class="dart-buffer" style="display:$display;"> |
| 381 <h3>Dart code for: ${escape(name)}</h3> |
| 382 ${dartCodeBuffer} |
| 383 </div>'''; |
| 384 } |
| 385 |
| 386 /// Computes a HTML visualization of the [codePoints]. |
| 387 String computeJsTraceHtmlPart(List<CodePoint> codePoints, |
| 388 SourceLocationCollection collection) { |
| 389 StringBuffer buffer = new StringBuffer(); |
| 390 buffer.write('<table style="width:100%;">'); |
| 391 buffer.write( |
| 392 '<tr><th>Node kind</th><th>JS code @ offset</th>' |
| 393 '<th>Dart code @ mapped location</th><th>file:position:name</th></tr>'); |
| 394 codePoints.forEach((CodePoint codePoint) { |
| 395 String jsCode = codePoint.jsCode; |
| 396 if (jsCode.length > 40) { |
| 397 jsCode = jsCode.substring(0, 40); |
| 398 } |
| 399 if (codePoint.sourceLocation != null) { |
| 400 int index = collection.getIndex(codePoint.sourceLocation); |
| 401 if (index != null) { |
| 402 |
| 403 buffer.write('<tr style="background:#${toColor(index)};" ' |
| 404 'name="trace$index" ' |
| 405 'onmouseover="highlight([${index}]);"' |
| 406 'onmouseout="highlight([]);">'); |
| 407 } else { |
| 408 buffer.write('<tr>'); |
| 409 print('${codePoint.sourceLocation} not found in '); |
| 410 collection.sourceLocationIndexMap.keys |
| 411 .where((l) => l.sourceUri == codePoint.sourceLocation.sourceUri) |
| 412 .forEach((l) => print(' $l')); |
| 413 } |
| 414 } else { |
| 415 buffer.write('<tr>'); |
| 416 } |
| 417 buffer.write('<td>${codePoint.kind}</td>'); |
| 418 buffer.write('<td class="code">${jsCode}</td>'); |
| 419 if (codePoint.sourceLocation == null) { |
| 420 //buffer.write('<td></td>'); |
| 421 } else { |
| 422 buffer.write('<td class="code">${codePoint.dartCode}</td>'); |
| 423 buffer.write('<td>${escape(codePoint.sourceLocation.shortText)}</td>'); |
| 424 } |
| 425 buffer.write('</tr>'); |
| 426 }); |
| 427 buffer.write('</table>'); |
| 428 |
| 429 return buffer.toString(); |
| 430 } |
OLD | NEW |