| // 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">${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(); |
| } |