blob: e4b8c95bf964085003cf6221d088c630b0b1134e [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 'dart:math' as Math;
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';
import 'html_parts.dart';
/// Truncate [input] to [length], adding '...' if truncated.
String truncate(String input, int length) {
if (input.length > length) {
return '${input.substring(0, length - 3)}...';
}
return input;
}
const int HUE_COUNT = 24;
/// Returns the [index]th color for visualization.
HSV toColor(int index) {
double h = 360.0 * (index % HUE_COUNT) / HUE_COUNT;
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. If [width] is provided, shorter
/// line numbers will be prefixed with spaces to match the width.
String lineNumber(int index,
{int width, bool useNbsp: false, String className}) {
if (className == null) {
className = 'lineNumber';
}
String text = '${index + 1}';
String padding = useNbsp ? ' ' : ' ';
if (width != null && text.length < width) {
text = (padding * (width - text.length)) + text;
}
return '<span class="$className">$text$padding</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);
String toString() {
return sourceMapInfo.toString();
}
}
/// 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];
}
}
abstract class CssColorScheme {
String singleLocationToCssColor(int id);
String multiLocationToCssColor(List<int> ids);
bool get showLocationAsSpan;
}
class CustomColorScheme implements CssColorScheme {
final bool showLocationAsSpan;
final Function single;
final Function multi;
CustomColorScheme(
{this.showLocationAsSpan: false,
String this.single(int id),
String this.multi(List<int> ids)});
String singleLocationToCssColor(int id) => single != null ? single(id) : null;
String multiLocationToCssColor(List<int> ids) =>
multi != null ? multi(ids) : null;
}
class PatternCssColorScheme implements CssColorScheme {
const PatternCssColorScheme();
bool get showLocationAsSpan => true;
String singleLocationToCssColor(int index) {
return "background:${toPattern(index)};";
}
String multiLocationToCssColor(List<int> indices) {
StringBuffer sb = new StringBuffer();
double delta = 100.0 / (indices.length);
double position = 0.0;
void addColor(String color) {
sb.write(', ${color} ${position.toInt()}%');
position += delta;
sb.write(', ${color} ${position.toInt()}%');
}
for (int index in indices) {
addColor('${toColorCss(index)}');
}
return 'background: linear-gradient(to right${sb}); '
'background-size: 10px 10px;';
}
}
class SingleColorScheme implements CssColorScheme {
const SingleColorScheme();
bool get showLocationAsSpan => false;
String singleLocationToCssColor(int index) {
return "background:${toColorCss(index)};";
}
String multiLocationToCssColor(List<int> indices) {
StringBuffer sb = new StringBuffer();
double delta = 100.0 / (indices.length);
double position = 0.0;
void addColor(String color) {
sb.write(', ${color} ${position.toInt()}%');
position += delta;
sb.write(', ${color} ${position.toInt()}%');
}
for (int index in indices) {
addColor('${toColorCss(index)}');
}
return 'background: linear-gradient(to bottom${sb}); '
'background-size: 10px 3px;';
}
}
/// 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 name;
int currentJsSourceOffset = 0;
final SourceLocationCollection collection;
final Map<int, List<SourceLocation>> codeLocations = {};
final CssColorScheme colorScheme;
CodeProcessor(this.name, this.collection,
{this.colorScheme: const PatternCssColorScheme()});
void addSourceLocation(int targetOffset, SourceLocation sourceLocation) {
codeLocations.putIfAbsent(targetOffset, () => []).add(sourceLocation);
collection.registerSourceLocation(sourceLocation);
}
String convertToHtml(String text) {
List<Annotation> annotations = <Annotation>[];
codeLocations.forEach((int codeOffset, List<SourceLocation> locations) {
for (SourceLocation location in locations) {
if (location != null) {
annotations.add(new Annotation(
collection.getIndex(location), codeOffset, location.shortText));
}
}
});
return convertAnnotatedCodeToHtml(text, annotations,
colorScheme: colorScheme,
elementScheme: new HighlightLinkScheme(name),
windowSize: 3);
}
}
class ElementScheme {
const ElementScheme();
String getName(int id, Set<int> ids) => null;
String getHref(int id, Set<int> ids) => null;
String onClick(int id, Set<int> ids) => null;
String onMouseOver(int id, Set<int> ids) => null;
String onMouseOut(int id, Set<int> ids) => null;
}
class HighlightLinkScheme implements ElementScheme {
final String name;
HighlightLinkScheme(this.name);
@override
String getName(int id, Set<int> indices) {
return 'js$id';
}
@override
String getHref(int id, Set<int> indices) {
return "#${id}";
}
@override
String onClick(int id, Set<int> indices) {
return "show(\'$name\');";
}
@override
String onMouseOut(int id, Set<int> indices) {
return "highlight([]);";
}
@override
String onMouseOver(int id, Set<int> indices) {
String onmouseover = indices.map((i) => '\'$i\'').join(',');
return "highlight([${onmouseover}]);";
}
}
String convertAnnotatedCodeToHtml(String code, Iterable<Annotation> annotations,
{CssColorScheme colorScheme: const SingleColorScheme(),
ElementScheme elementScheme: const ElementScheme(),
int windowSize}) {
StringBuffer htmlBuffer = new StringBuffer();
List<CodeLine> lines = convertAnnotatedCodeToCodeLines(code, annotations,
windowSize: windowSize);
int lineNoWidth;
if (lines.isNotEmpty) {
lineNoWidth = '${lines.last.lineNo + 1}'.length;
}
HtmlPrintContext context = new HtmlPrintContext(
lineNoWidth: lineNoWidth,
getAnnotationData: createAnnotationDataFunction(
colorScheme: colorScheme, elementScheme: elementScheme));
for (CodeLine line in lines) {
line.printHtmlOn(htmlBuffer, context);
}
return htmlBuffer.toString();
}
List<CodeLine> convertAnnotatedCodeToCodeLines(
String code, Iterable<Annotation> annotations,
{int startLine, int endLine, int windowSize, Uri uri}) {
List<CodeLine> lines = <CodeLine>[];
CodeLine currentLine;
final List<Annotation> currentAnnotations = <Annotation>[];
int offset = 0;
int lineIndex = 0;
int firstLine;
int lastLine;
void addCode(String code) {
if (currentLine != null) {
currentLine.codeBuffer.write(code);
currentLine.codeParts
.add(new CodePart(currentAnnotations.toList(), code));
currentAnnotations.clear();
}
}
void addAnnotations(List<Annotation> annotations) {
currentAnnotations.addAll(annotations);
if (currentLine != null) {
currentLine.annotations.addAll(annotations);
}
}
void beginLine(int currentOffset) {
lines
.add(currentLine = new CodeLine(lines.length, currentOffset, uri: uri));
}
void endCurrentLocation() {
if (currentAnnotations.isNotEmpty) {
addCode('');
}
}
void addSubstring(int until, {bool isFirst: false, bool isLast: false}) {
if (until <= offset) return;
if (offset >= code.length) return;
String substring = code.substring(offset, until);
bool first = true;
if (isLast) {
lastLine = lineIndex;
}
int localOffset = 0;
if (isFirst) {
beginLine(offset + localOffset);
}
for (String line in substring.split('\n')) {
if (!first) {
endCurrentLocation();
lineIndex++;
beginLine(offset + localOffset);
}
addCode(line);
first = false;
localOffset += line.length + 1;
}
if (isFirst) {
firstLine = lineIndex;
}
offset = until;
}
void insertAnnotations(List<Annotation> annotations) {
endCurrentLocation();
addAnnotations(annotations);
if (annotations.last == null) {
endCurrentLocation();
}
}
Map<int, List<Annotation>> annotationMap = <int, List<Annotation>>{};
for (Annotation annotation in annotations) {
annotationMap
.putIfAbsent(annotation.codeOffset, () => <Annotation>[])
.add(annotation);
}
bool first = true;
for (int codeOffset in annotationMap.keys.toList()..sort()) {
List<Annotation> annotationList = annotationMap[codeOffset];
addSubstring(codeOffset, isFirst: first);
insertAnnotations(annotationList);
first = false;
}
addSubstring(code.length, isFirst: first, isLast: true);
endCurrentLocation();
int start = startLine ?? 0;
int end = endLine ?? lines.length - 1;
if (lastLine == 0) lastLine = firstLine;
if (windowSize != null) {
start = Math.max(firstLine - windowSize, start);
end = Math.min(lastLine + windowSize, end);
}
return lines.sublist(start, end);
}
/// 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) {
String name = info.name;
SourceLocationCollection subcollection =
new SourceLocationCollection(collection);
CodeProcessor codeProcessor = new CodeProcessor(name, 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) {
if (sourceLocation.sourceUri == null || sourceLocation.line == null) return;
Map<int, List<SourceLocation>> uriMap =
sourceLocationMap.putIfAbsent(sourceLocation.sourceUri, () => {});
List<SourceLocation> lineList =
uriMap.putIfAbsent(sourceLocation.line - 1, () => []);
lineList.add(sourceLocation);
});
sourceLocationMap.forEach((Uri uri, Map<int, List<SourceLocation>> uriMap) {
SourceFile sourceFile = sourceFileManager.getSourceFile(uri);
if (sourceFile == null) {
print('No source file for $uri');
return;
}
StringBuffer codeBuffer = new StringBuffer();
int firstLineIndex;
int lastLineIndex;
List<int> lineIndices = uriMap.keys.toList()..sort();
int lineNoWidth;
if (lineIndices.isNotEmpty) {
lineNoWidth = '${lineIndices.last + windowSize + 1}'.length;
}
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');
dartCodeBuffer.write('<p class="line">');
for (int line = firstLineIndex - windowSize;
line < firstLineIndex;
line++) {
if (line >= 0) {
dartCodeBuffer.write('</p><p class="line">');
dartCodeBuffer.write(lineNumber(line, width: lineNoWidth));
dartCodeBuffer.write(sourceFile.kernelSource.getTextLine(line + 1));
}
}
dartCodeBuffer.write(codeBuffer);
for (int line = lastLineIndex + 1;
line <= lastLineIndex + windowSize;
line++) {
if (line < sourceFile.lines) {
dartCodeBuffer.write('</p><p class="line">');
dartCodeBuffer.write(lineNumber(line, width: lineNoWidth));
dartCodeBuffer.write(sourceFile.kernelSource.getTextLine(line + 1));
}
}
dartCodeBuffer.write('</p>');
dartCodeBuffer.write('</pre>\n');
firstLineIndex = null;
lastLineIndex = null;
}
codeBuffer.clear();
}
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('</p><p class="line">');
codeBuffer.write(lineNumber(line, width: lineNoWidth));
codeBuffer.write(sourceFile.kernelSource.getTextLine(line + 1));
}
}
String line = sourceFile.kernelSource.getTextLine(lineIndex + 1);
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 - 1;
int end = line.length;
if (i + 1 < locations.length) {
end = locations[i + 1].column - 1;
}
if (i == 0) {
codeBuffer.write('</p><p class="line">');
codeBuffer.write(lineNumber(lineIndex, width: lineNoWidth));
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 = truncate(codePoint.jsCode, 50);
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">${codePoint.targetOffset}:${jsCode}</td>');
if (codePoint.sourceLocation == null) {
//buffer.write('<td></td>');
} else {
String dartCode = truncate(codePoint.dartCode, 50);
buffer.write('<td class="code">${dartCode}</td>');
buffer.write('<td>${escape(codePoint.sourceLocation.shortText)}</td>');
}
buffer.write('</tr>');
});
buffer.write('</table>');
return buffer.toString();
}