// 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 '$text$padding'; } /// 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 sourceLocations = []; Map 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 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 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 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> 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 annotations = []; codeLocations.forEach((int codeOffset, List 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 ids) => null; String? getHref(int id, Set ids) => null; String? onClick(int id, Set ids) => null; String? onMouseOver(int id, Set ids) => null; String? onMouseOut(int id, Set ids) => null; } class HighlightLinkScheme implements ElementScheme { final String name; HighlightLinkScheme(this.name); @override String getName(int id, Set indices) { return 'js$id'; } @override String getHref(int id, Set indices) { return "#${id}"; } @override String onClick(int id, Set indices) { return "show(\'$name\');"; } @override String onMouseOut(int id, Set indices) { return "highlight([]);"; } @override String onMouseOver(int id, Set indices) { String onmouseover = indices.map((i) => '\'$i\'').join(','); return "highlight([${onmouseover}]);"; } } String convertAnnotatedCodeToHtml( String code, Iterable annotations, { CssColorScheme colorScheme = const SingleColorScheme(), required ElementScheme elementScheme, required int windowSize, }) { StringBuffer htmlBuffer = StringBuffer(); List 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 convertAnnotatedCodeToCodeLines( String code, Iterable annotations, { int? startLine, int? endLine, int? windowSize, Uri? uri, }) { List lines = []; CodeLine? currentLine; final List currentAnnotations = []; 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 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 annotations) { endCurrentLocation(); addAnnotations(annotations); } Map> annotationMap = >{}; for (Annotation annotation in annotations) { annotationMap .putIfAbsent(annotation.codeOffset, () => []) .add(annotation); } bool first = true; for (int codeOffset in annotationMap.keys.toList()..sort()) { List 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 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( '

JS code for: ${escape(name)}

\n', ); jsCodeBuffer.write('''
$html
'''); } return jsCodeBuffer.toString(); } /// Computes the HTML representation of the source mapping information for a /// collection of JavaScript code blocks. String computeJsTraceHtml(Iterable infoList) { StringBuffer jsTraceBuffer = StringBuffer(); for (SourceMapHtmlInfo info in infoList) { String name = info.sourceMapInfo.name!; String jsTrace = computeJsTraceHtmlPart( info.sourceMapInfo.codePoints, info.sourceLocationCollection, ); jsTraceBuffer.write(''' '''); } 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 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 infoList, ) { SourceFileManager sourceFileManager = sourceMapProcessor.sourceFileManager; SourceLocationCollection collection = SourceLocationCollection(); List htmlInfoList = []; 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 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>> sourceLocationMap = {}; collection.sourceLocations.forEach((SourceLocation sourceLocation) { if (sourceLocation.sourceUri == null) return; Map> uriMap = sourceLocationMap.putIfAbsent( sourceLocation.sourceUri!, () => {}, ); List lineList = uriMap.putIfAbsent( sourceLocation.line - 1, () => [], ); lineList.add(sourceLocation); }); sourceLocationMap.forEach((Uri uri, Map> uriMap) { SourceFile? sourceFile = sourceFileManager.getSourceFile(uri); if (sourceFile == null) { print('No source file for $uri'); return; } StringBuffer codeBuffer = StringBuffer(); int? firstLineIndex; int? lastLineIndex; List 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( '

${uri.pathSegments.last}, ' '${firstLineIndex! - windowSize + 1}-' '${lastLineIndex! + windowSize + 1}' '

\n', ); dartCodeBuffer.write('
\n');
        dartCodeBuffer.write('

'); for ( int line = firstLineIndex! - windowSize; line < firstLineIndex!; line++ ) { if (line >= 0) { dartCodeBuffer.write('

'); 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('

'); dartCodeBuffer.write(lineNumber(line, width: lineNoWidth)); dartCodeBuffer.write(sourceFile.kernelSource.getTextLine(line + 1)); } } dartCodeBuffer.write('

'); dartCodeBuffer.write('
\n'); firstLineIndex = null; lastLineIndex = null; } codeBuffer.clear(); } lineIndices.forEach((int lineIndex) { List 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('

'); 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('

'); codeBuffer.write(lineNumber(lineIndex, width: lineNoWidth)); codeBuffer.write(line.substring(0, start)); } codeBuffer.write( '', ); codeBuffer.write(line.substring(start, end)); codeBuffer.write(''); } lastLineIndex = lineIndex; }); flush(); }); String display = showAsBlock ? 'block' : 'none'; return '''

Dart code for: ${escape(name)}

${dartCodeBuffer}
'''; } /// Computes a HTML visualization of the [codePoints]. String computeJsTraceHtmlPart( List codePoints, SourceLocationCollection collection, ) { StringBuffer buffer = StringBuffer(); buffer.write(''); buffer.write( '' '', ); 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( '', ); } else { buffer.write(''); print('${codePoint.sourceLocation} not found in '); collection.sourceLocationIndexMap.keys .where((l) => l.sourceUri == codePoint.sourceLocation!.sourceUri) .forEach((l) => print(' $l')); } } else { buffer.write(''); } buffer.write(''); buffer.write(''); if (codePoint.sourceLocation == null) { //buffer.write(''); } else { String dartCode = truncate(codePoint.dartCode!, 50); buffer.write(''); buffer.write(''); } buffer.write(''); }); buffer.write('
Node kindJS code @ offsetDart code @ mapped locationfile:position:name
${codePoint.kind}${codePoint.targetOffset}:${jsCode}${dartCode}${escape(codePoint.sourceLocation!.shortText)}
'); return buffer.toString(); }