| // 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 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 = 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, |
| ); |
| |
| @override |
| 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 PatternCssColorScheme implements CssColorScheme { |
| const PatternCssColorScheme(); |
| |
| @override |
| bool get showLocationAsSpan => true; |
| |
| @override |
| String singleLocationToCssColor(int index) { |
| return "background:${toPattern(index)};"; |
| } |
| |
| @override |
| String multiLocationToCssColor(List<int> indices) { |
| StringBuffer sb = 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(); |
| |
| @override |
| bool get showLocationAsSpan => false; |
| |
| @override |
| String singleLocationToCssColor(int index) { |
| return "background:${toColorCss(index)};"; |
| } |
| |
| @override |
| String multiLocationToCssColor(List<int> indices) { |
| StringBuffer sb = 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) { |
| annotations.add( |
| new Annotation( |
| collection.getIndex(location), |
| codeOffset, |
| location.shortText, |
| ), |
| ); |
| } |
| }); |
| return convertAnnotatedCodeToHtml( |
| text, |
| annotations, |
| colorScheme: colorScheme, |
| elementScheme: 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(), |
| required ElementScheme elementScheme, |
| required int windowSize, |
| }) { |
| StringBuffer htmlBuffer = StringBuffer(); |
| List<CodeLine> lines = convertAnnotatedCodeToCodeLines( |
| code, |
| annotations, |
| windowSize: windowSize, |
| ); |
| int? lineNoWidth; |
| if (lines.isNotEmpty) { |
| lineNoWidth = '${lines.last.lineNo + 1}'.length; |
| } |
| HtmlPrintContext context = 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; |
| late final int firstLine; |
| late final 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 = 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); |
| } |
| |
| 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 = 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 = 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 = SourceLocationCollection(collection); |
| CodeProcessor codeProcessor = 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 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 = 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 = 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 = StringBuffer(); |
| Map<Uri, Map<int, List<SourceLocation>>> sourceLocationMap = {}; |
| collection.sourceLocations.forEach((SourceLocation sourceLocation) { |
| if (sourceLocation.sourceUri == 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 = 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 = 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!); |
| 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(); |
| } |