blob: 4227fee724ff4bf7c43a26b202ea79efe0d20070 [file] [log] [blame]
// 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.
HSV toColor(int index) {
int hueCount = 24;
double h = 360.0 * (index % hueCount) / hueCount;
double v = 1.0;
double s = 0.5;
return new HSV(h, s, v);
}
/// Return the CSS color value for the [index]th color.
String toColorCss(int index) {
return toColor(index).toCss;
}
/// Return the CSS color value for the [index]th span.
String toPattern(int index) {
/// Use gradient on spans to visually identify consecutive spans mapped to the
/// same source location.
HSV startColor = toColor(index);
HSV endColor = new HSV(startColor.h, startColor.s + 0.4, startColor.v - 0.2);
return 'linear-gradient(to right, ${startColor.toCss}, ${endColor.toCss})';
}
/// 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:${toPattern(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('${toColorCss(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:${toPattern(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 (codePoint.sourceLocation != null) {
int index = collection.getIndex(codePoint.sourceLocation);
if (index != null) {
String style = '';
if (!codePoint.isMissing) {
style = 'style="background:${toColorCss(index)};" ';
}
buffer.write('<tr $style'
'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();
}