Index: tests/compiler/dart2js/sourcemaps/sourcemap_html_helper.dart |
diff --git a/tests/compiler/dart2js/sourcemaps/sourcemap_html_helper.dart b/tests/compiler/dart2js/sourcemaps/sourcemap_html_helper.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..eb1399a60628b27a89c1a1d210f2e53afb719c38 |
--- /dev/null |
+++ b/tests/compiler/dart2js/sourcemaps/sourcemap_html_helper.dart |
@@ -0,0 +1,430 @@ |
+// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file |
+// for details. All rights reserved. Use of this source code is governed by a |
+// BSD-style license that can be found in the LICENSE file. |
+ |
+/// Helper for creating HTML visualization of the source map information |
+/// generated by a [SourceMapProcessor]. |
+ |
+library sourcemap.html.helper; |
+ |
+import 'dart:convert'; |
+ |
+import 'package:compiler/src/io/source_file.dart'; |
+import 'package:compiler/src/io/source_information.dart'; |
+import 'package:compiler/src/js/js.dart' as js; |
+ |
+import 'colors.dart'; |
+import 'sourcemap_helper.dart'; |
+import 'sourcemap_html_templates.dart'; |
+ |
+/// Returns the [index]th color for visualization. |
+String toColor(int index) { |
+ int hueCount = 24; |
+ double h = 360.0 * (index % hueCount) / hueCount; |
+ double v = 1.0; |
+ double s = 0.5; |
+ HSV hsv = new HSV(h, s, v); |
+ RGB rgb = HSV.toRGB(hsv); |
+ return rgb.toHex; |
+} |
+ |
+/// Return the html for the [index] line number. |
+String lineNumber(int index) { |
+ return '<span class="lineNumber">${index + 1} </span>'; |
+} |
+ |
+/// Return the html escaped [text]. |
+String escape(String text) { |
+ return const HtmlEscape().convert(text); |
+} |
+ |
+/// Information needed to generate HTML for a single [SourceMapInfo]. |
+class SourceMapHtmlInfo { |
+ final SourceMapInfo sourceMapInfo; |
+ final CodeProcessor codeProcessor; |
+ final SourceLocationCollection sourceLocationCollection; |
+ |
+ SourceMapHtmlInfo(this.sourceMapInfo, |
+ this.codeProcessor, |
+ this.sourceLocationCollection); |
+} |
+ |
+/// A collection of source locations. |
+/// |
+/// Used to index source locations for visualization and linking. |
+class SourceLocationCollection { |
+ List<SourceLocation> sourceLocations = []; |
+ Map<SourceLocation, int> sourceLocationIndexMap; |
+ |
+ SourceLocationCollection([SourceLocationCollection parent]) |
+ : sourceLocationIndexMap = |
+ parent == null ? {} : parent.sourceLocationIndexMap; |
+ |
+ int registerSourceLocation(SourceLocation sourceLocation) { |
+ return sourceLocationIndexMap.putIfAbsent(sourceLocation, () { |
+ sourceLocations.add(sourceLocation); |
+ return sourceLocationIndexMap.length; |
+ }); |
+ } |
+ |
+ int getIndex(SourceLocation sourceLocation) { |
+ return sourceLocationIndexMap[sourceLocation]; |
+ } |
+} |
+ |
+/// Processor that computes the HTML representation of a block of JavaScript |
+/// code and collects the source locations mapped in the code. |
+class CodeProcessor { |
+ int lineIndex = 0; |
+ final String onclick; |
+ int currentJsSourceOffset = 0; |
+ final SourceLocationCollection collection; |
+ final Map<int, List<SourceLocation>> codeLocations = {}; |
+ |
+ CodeProcessor(this.onclick, this.collection); |
+ |
+ void addSourceLocation(int targetOffset, SourceLocation sourceLocation) { |
+ codeLocations.putIfAbsent(targetOffset, () => []).add(sourceLocation); |
+ collection.registerSourceLocation(sourceLocation); |
+ } |
+ |
+ String convertToHtml(String text) { |
+ StringBuffer htmlBuffer = new StringBuffer(); |
+ int offset = 0; |
+ int lineIndex = 0; |
+ bool pendingSourceLocationsEnd = false; |
+ htmlBuffer.write(lineNumber(lineIndex)); |
+ SourceLocation currentLocation; |
+ |
+ void endCurrentLocation() { |
+ if (currentLocation != null) { |
+ htmlBuffer.write('</a>'); |
+ } |
+ currentLocation = null; |
+ } |
+ |
+ void addSubstring(int until) { |
+ if (until <= offset) return; |
+ |
+ String substring = text.substring(offset, until); |
+ offset = until; |
+ bool first = true; |
+ for (String line in substring.split('\n')) { |
+ if (!first) { |
+ endCurrentLocation(); |
+ htmlBuffer.write('\n'); |
+ lineIndex++; |
+ htmlBuffer.write(lineNumber(lineIndex)); |
+ } |
+ htmlBuffer.write(escape(line)); |
+ first = false; |
+ } |
+ } |
+ |
+ void insertSourceLocations(List<SourceLocation> lastSourceLocations) { |
+ endCurrentLocation(); |
+ |
+ String color; |
+ int index; |
+ String title; |
+ if (lastSourceLocations.length == 1) { |
+ SourceLocation sourceLocation = lastSourceLocations.single; |
+ if (sourceLocation != null) { |
+ index = collection.getIndex(sourceLocation); |
+ color = "background:#${toColor(index)};"; |
+ title = sourceLocation.shortText; |
+ currentLocation = sourceLocation; |
+ } |
+ } else { |
+ |
+ index = collection.getIndex(lastSourceLocations.first); |
+ StringBuffer sb = new StringBuffer(); |
+ double delta = 100.0 / (lastSourceLocations.length); |
+ double position = 0.0; |
+ |
+ void addColor(String color) { |
+ sb.write(', ${color} ${position.toInt()}%'); |
+ position += delta; |
+ sb.write(', ${color} ${position.toInt()}%'); |
+ } |
+ |
+ for (SourceLocation sourceLocation in lastSourceLocations) { |
+ if (sourceLocation == null) continue; |
+ int colorIndex = collection.getIndex(sourceLocation); |
+ addColor('#${toColor(colorIndex)}'); |
+ currentLocation = sourceLocation; |
+ } |
+ color = 'background: linear-gradient(to right${sb}); ' |
+ 'background-size: 10px 10px;'; |
+ title = lastSourceLocations.map((l) => l.shortText).join(','); |
+ } |
+ if (index != null) { |
+ Set<int> indices = |
+ lastSourceLocations.map((l) => collection.getIndex(l)).toSet(); |
+ String onmouseover = indices.map((i) => '\'$i\'').join(','); |
+ htmlBuffer.write( |
+ '<a name="js$index" href="#${index}" style="$color" title="$title" ' |
+ 'onclick="${onclick}" onmouseover="highlight([${onmouseover}]);"' |
+ 'onmouseout="highlight([]);">'); |
+ pendingSourceLocationsEnd = true; |
+ } |
+ if (lastSourceLocations.last == null) { |
+ endCurrentLocation(); |
+ } |
+ } |
+ |
+ for (int targetOffset in codeLocations.keys.toList()..sort()) { |
+ List<SourceLocation> sourceLocations = codeLocations[targetOffset]; |
+ addSubstring(targetOffset); |
+ insertSourceLocations(sourceLocations); |
+ } |
+ |
+ addSubstring(text.length); |
+ endCurrentLocation(); |
+ return htmlBuffer.toString(); |
+ } |
+} |
+ |
+/// Computes the HTML representation for a collection of JavaScript code blocks. |
+String computeJsHtml(Iterable<SourceMapHtmlInfo> infoList) { |
+ |
+ StringBuffer jsCodeBuffer = new StringBuffer(); |
+ for (SourceMapHtmlInfo info in infoList) { |
+ String name = info.sourceMapInfo.name; |
+ String html = info.codeProcessor.convertToHtml(info.sourceMapInfo.code); |
+ String onclick = 'show(\'$name\');'; |
+ jsCodeBuffer.write( |
+ '<h3 onclick="$onclick">JS code for: ${escape(name)}</h3>\n'); |
+ jsCodeBuffer.write(''' |
+<pre> |
+$html |
+</pre> |
+'''); |
+ } |
+ return jsCodeBuffer.toString(); |
+} |
+ |
+/// Computes the HTML representation of the source mapping information for a |
+/// collection of JavaScript code blocks. |
+String computeJsTraceHtml(Iterable<SourceMapHtmlInfo> infoList) { |
+ StringBuffer jsTraceBuffer = new StringBuffer(); |
+ for (SourceMapHtmlInfo info in infoList) { |
+ String name = info.sourceMapInfo.name; |
+ String jsTrace = computeJsTraceHtmlPart( |
+ info.sourceMapInfo.codePoints, info.sourceLocationCollection); |
+ jsTraceBuffer.write(''' |
+<div name="$name" class="js-trace-buffer" style="display:none;"> |
+<h3>Trace for: ${escape(name)}</h3> |
+$jsTrace |
+</div> |
+'''); |
+ } |
+ return jsTraceBuffer.toString(); |
+} |
+ |
+/// Computes the HTML information for the [info]. |
+SourceMapHtmlInfo createHtmlInfo(SourceLocationCollection collection, |
+ SourceMapInfo info) { |
+ js.Node node = info.node; |
+ String code = info.code; |
+ String name = info.name; |
+ String onclick = 'show(\'$name\');'; |
+ SourceLocationCollection subcollection = |
+ new SourceLocationCollection(collection); |
+ CodeProcessor codeProcessor = new CodeProcessor(onclick, subcollection); |
+ for (js.Node node in info.nodeMap.nodes) { |
+ info.nodeMap[node].forEach( |
+ (int targetOffset, List<SourceLocation> sourceLocations) { |
+ for (SourceLocation sourceLocation in sourceLocations) { |
+ codeProcessor.addSourceLocation(targetOffset, sourceLocation); |
+ } |
+ }); |
+ } |
+ return new SourceMapHtmlInfo(info, codeProcessor, subcollection); |
+} |
+ |
+/// Outputs a HTML file in [jsMapHtmlUri] containing an interactive |
+/// visualization of the source mapping information in [infoList] computed |
+/// with the [sourceMapProcessor]. |
+void createTraceSourceMapHtml(Uri jsMapHtmlUri, |
+ SourceMapProcessor sourceMapProcessor, |
+ Iterable<SourceMapInfo> infoList) { |
+ SourceFileManager sourceFileManager = sourceMapProcessor.sourceFileManager; |
+ SourceLocationCollection collection = new SourceLocationCollection(); |
+ List<SourceMapHtmlInfo> htmlInfoList = <SourceMapHtmlInfo>[]; |
+ for (SourceMapInfo info in infoList) { |
+ htmlInfoList.add(createHtmlInfo(collection, info)); |
+ } |
+ |
+ String jsCode = computeJsHtml(htmlInfoList); |
+ String dartCode = computeDartHtml(sourceFileManager, htmlInfoList); |
+ |
+ String jsTraceHtml = computeJsTraceHtml(htmlInfoList); |
+ outputJsDartTrace(jsMapHtmlUri, jsCode, dartCode, jsTraceHtml); |
+ print('Trace source map html generated: $jsMapHtmlUri'); |
+} |
+ |
+/// Computes the HTML representation for the Dart code snippets referenced in |
+/// [infoList]. |
+String computeDartHtml( |
+ SourceFileManager sourceFileManager, |
+ Iterable<SourceMapHtmlInfo> infoList) { |
+ |
+ StringBuffer dartCodeBuffer = new StringBuffer(); |
+ for (SourceMapHtmlInfo info in infoList) { |
+ dartCodeBuffer.write(computeDartHtmlPart(info.sourceMapInfo.name, |
+ sourceFileManager, info.sourceLocationCollection)); |
+ } |
+ return dartCodeBuffer.toString(); |
+ |
+} |
+ |
+/// Computes the HTML representation for the Dart code snippets in [collection]. |
+String computeDartHtmlPart(String name, |
+ SourceFileManager sourceFileManager, |
+ SourceLocationCollection collection, |
+ {bool showAsBlock: false}) { |
+ const int windowSize = 3; |
+ StringBuffer dartCodeBuffer = new StringBuffer(); |
+ Map<Uri, Map<int, List<SourceLocation>>> sourceLocationMap = {}; |
+ collection.sourceLocations.forEach((SourceLocation sourceLocation) { |
+ Map<int, List<SourceLocation>> uriMap = |
+ sourceLocationMap.putIfAbsent(sourceLocation.sourceUri, () => {}); |
+ List<SourceLocation> lineList = |
+ uriMap.putIfAbsent(sourceLocation.line, () => []); |
+ lineList.add(sourceLocation); |
+ }); |
+ sourceLocationMap.forEach((Uri uri, Map<int, List<SourceLocation>> uriMap) { |
+ SourceFile sourceFile = sourceFileManager.getSourceFile(uri); |
+ StringBuffer codeBuffer = new StringBuffer(); |
+ |
+ int firstLineIndex; |
+ int lastLineIndex; |
+ |
+ void flush() { |
+ if (firstLineIndex != null && lastLineIndex != null) { |
+ dartCodeBuffer.write( |
+ '<h4>${uri.pathSegments.last}, ' |
+ '${firstLineIndex - windowSize + 1}-' |
+ '${lastLineIndex + windowSize + 1}' |
+ '</h4>\n'); |
+ dartCodeBuffer.write('<pre>\n'); |
+ for (int line = firstLineIndex - windowSize; |
+ line < firstLineIndex; |
+ line++) { |
+ if (line >= 0) { |
+ dartCodeBuffer.write(lineNumber(line)); |
+ dartCodeBuffer.write(sourceFile.getLineText(line)); |
+ } |
+ } |
+ dartCodeBuffer.write(codeBuffer); |
+ for (int line = lastLineIndex + 1; |
+ line <= lastLineIndex + windowSize; |
+ line++) { |
+ if (line < sourceFile.lines) { |
+ dartCodeBuffer.write(lineNumber(line)); |
+ dartCodeBuffer.write(sourceFile.getLineText(line)); |
+ } |
+ } |
+ dartCodeBuffer.write('</pre>\n'); |
+ firstLineIndex = null; |
+ lastLineIndex = null; |
+ } |
+ codeBuffer.clear(); |
+ } |
+ |
+ List<int> lineIndices = uriMap.keys.toList()..sort(); |
+ lineIndices.forEach((int lineIndex) { |
+ List<SourceLocation> locations = uriMap[lineIndex]; |
+ if (lastLineIndex != null && |
+ lastLineIndex + windowSize * 4 < lineIndex) { |
+ flush(); |
+ } |
+ if (firstLineIndex == null) { |
+ firstLineIndex = lineIndex; |
+ } else { |
+ for (int line = lastLineIndex + 1; line < lineIndex; line++) { |
+ codeBuffer.write(lineNumber(line)); |
+ codeBuffer.write(sourceFile.getLineText(line)); |
+ } |
+ } |
+ String line = sourceFile.getLineText(lineIndex); |
+ locations.sort((a, b) => a.offset.compareTo(b.offset)); |
+ for (int i = 0; i < locations.length; i++) { |
+ SourceLocation sourceLocation = locations[i]; |
+ int index = collection.getIndex(sourceLocation); |
+ int start = sourceLocation.column; |
+ int end = line.length; |
+ if (i + 1 < locations.length) { |
+ end = locations[i + 1].column; |
+ } |
+ if (i == 0) { |
+ codeBuffer.write(lineNumber(lineIndex)); |
+ codeBuffer.write(line.substring(0, start)); |
+ } |
+ codeBuffer.write( |
+ '<a name="${index}" style="background:#${toColor(index)};" ' |
+ 'title="[${lineIndex + 1},${start + 1}]" ' |
+ 'onmouseover="highlight(\'$index\');" ' |
+ 'onmouseout="highlight();">'); |
+ codeBuffer.write(line.substring(start, end)); |
+ codeBuffer.write('</a>'); |
+ } |
+ lastLineIndex = lineIndex; |
+ }); |
+ |
+ flush(); |
+ }); |
+ String display = showAsBlock ? 'block' : 'none'; |
+ return ''' |
+<div name="$name" class="dart-buffer" style="display:$display;"> |
+<h3>Dart code for: ${escape(name)}</h3> |
+${dartCodeBuffer} |
+</div>'''; |
+} |
+ |
+/// Computes a HTML visualization of the [codePoints]. |
+String computeJsTraceHtmlPart(List<CodePoint> codePoints, |
+ SourceLocationCollection collection) { |
+ StringBuffer buffer = new StringBuffer(); |
+ buffer.write('<table style="width:100%;">'); |
+ buffer.write( |
+ '<tr><th>Node kind</th><th>JS code @ offset</th>' |
+ '<th>Dart code @ mapped location</th><th>file:position:name</th></tr>'); |
+ codePoints.forEach((CodePoint codePoint) { |
+ String jsCode = codePoint.jsCode; |
+ if (jsCode.length > 40) { |
+ jsCode = jsCode.substring(0, 40); |
+ } |
+ if (codePoint.sourceLocation != null) { |
+ int index = collection.getIndex(codePoint.sourceLocation); |
+ if (index != null) { |
+ |
+ buffer.write('<tr style="background:#${toColor(index)};" ' |
+ 'name="trace$index" ' |
+ 'onmouseover="highlight([${index}]);"' |
+ 'onmouseout="highlight([]);">'); |
+ } else { |
+ buffer.write('<tr>'); |
+ print('${codePoint.sourceLocation} not found in '); |
+ collection.sourceLocationIndexMap.keys |
+ .where((l) => l.sourceUri == codePoint.sourceLocation.sourceUri) |
+ .forEach((l) => print(' $l')); |
+ } |
+ } else { |
+ buffer.write('<tr>'); |
+ } |
+ buffer.write('<td>${codePoint.kind}</td>'); |
+ buffer.write('<td class="code">${jsCode}</td>'); |
+ if (codePoint.sourceLocation == null) { |
+ //buffer.write('<td></td>'); |
+ } else { |
+ buffer.write('<td class="code">${codePoint.dartCode}</td>'); |
+ buffer.write('<td>${escape(codePoint.sourceLocation.shortText)}</td>'); |
+ } |
+ buffer.write('</tr>'); |
+ }); |
+ buffer.write('</table>'); |
+ |
+ return buffer.toString(); |
+} |