// 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. // @dart = 2.7 /// 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 '$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 CustomColorScheme implements CssColorScheme { @override final bool showLocationAsSpan; final Function single; final Function multi; CustomColorScheme( {this.showLocationAsSpan: false, String this.single(int id), String this.multi(List ids)}); @override String singleLocationToCssColor(int id) => single != null ? single(id) : null; @override String multiLocationToCssColor(List ids) => multi != null ? multi(ids) : null; } 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 = 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(); @override bool get showLocationAsSpan => false; @override String singleLocationToCssColor(int index) { return "background:${toColorCss(index)};"; } @override String multiLocationToCssColor(List 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> 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) { 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 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(), ElementScheme elementScheme: const ElementScheme(), int windowSize}) { StringBuffer htmlBuffer = new StringBuffer(); List 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 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; 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 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 annotations) { endCurrentLocation(); addAnnotations(annotations); if (annotations.last == null) { endCurrentLocation(); } } 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 = 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('

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 = new 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 = new SourceLocationCollection(collection); CodeProcessor codeProcessor = new 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 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 infoList) { SourceFileManager sourceFileManager = sourceMapProcessor.sourceFileManager; SourceLocationCollection collection = new 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 = 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>> sourceLocationMap = {}; collection.sourceLocations.forEach((SourceLocation sourceLocation) { if (sourceLocation.sourceUri == null || sourceLocation.line == 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 = new 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 = new 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); if (index != null) { 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(); }