Add syntax highlighting to instrumentation_renderer

In this change, we also push HTML escaping to mustache.

Changes can be seen in x20 pages emailed to the team.

Change-Id: I239668a0844fb5bd7c90848f25a3cbf29e50363d
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/118460
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Samuel Rawlins <srawlins@google.com>
This commit is contained in:
Sam Rawlins 2019-09-23 18:51:55 +00:00 committed by commit-bot@chromium.org
parent 0b7a78d0a6
commit 87a369f1d2
2 changed files with 118 additions and 43 deletions

View file

@ -15,31 +15,45 @@ class InstrumentationRenderer {
Map<String, dynamic> mustacheContext = {'units': <Map<String, dynamic>>[]};
for (var compilationUnit in info.units) {
StringBuffer buffer = StringBuffer();
// List of Mustache context for both unmodified and modified regions:
//
// * 'modified': Whether this region represents modified source, or
// unmodified.
// * 'content': The textual content of this region.
// * 'explanation': The textual explanation of why the content in this
// region was modified. It will appear in a "tooltip" on hover.
// TODO(srawlins): Support some sort of HTML explanation, with
// hyperlinks to anchors in other source code.
List<Map> regions = [];
for (var region in compilationUnit.regions) {
if (region.offset > previousIndex) {
// Display a region of unmodified content.
buffer.write(
compilationUnit.content.substring(previousIndex, region.offset));
regions.add({
'modified': false,
'content':
compilationUnit.content.substring(previousIndex, region.offset)
});
previousIndex = region.offset + region.length;
}
buffer.write(_regionWithTooltip(region, compilationUnit.content));
regions.add({
'modified': true,
'content': compilationUnit.content
.substring(region.offset, region.offset + region.length),
'explanation': region.explanation,
});
}
if (previousIndex < compilationUnit.content.length) {
// Last region of unmodified content.
buffer.write(compilationUnit.content.substring(previousIndex));
regions.add({
'modified': false,
'content': compilationUnit.content.substring(previousIndex)
});
}
mustacheContext['units']
.add({'path': compilationUnit.path, 'content': buffer.toString()});
.add({'path': compilationUnit.path, 'regions': regions});
}
return _template.renderString(mustacheContext);
}
String _regionWithTooltip(RegionInfo region, String content) {
String regionContent =
content.substring(region.offset, region.offset + region.length);
return '<span class="region">$regionContent'
'<span class="tooltip">${region.explanation}</span></span>';
}
}
/// A mustache template for one library's instrumentation output.
@ -47,35 +61,58 @@ mustache.Template _template = mustache.Template(r'''
<html>
<head>
<title>Non-nullable fix instrumentation report</title>
<script src="highlight.pack.js"></script>
<link rel="stylesheet" href="styles/androidstudio.css">
<style>
body {
font-family: sans-serif;
padding: 1em;
}
h2 {
font-size: 1em;
font-weight: bold;
}
div.content {
.content {
font-family: monospace;
whitespace: pre;
white-space: pre;
}
.content.highlighting {
position: relative;
}
.regions {
position: absolute;
top: 0.5em;
/* The content of the regions is not visible; the user instead will see the
* highlighted copy of the content. */
visibility: hidden;
}
.region {
/* Green means this region was added. */
color: green;
background-color: #ccffcc;
color: #003300;
cursor: default;
display: inline-block;
position: relative;
visibility: visible;
}
.region .tooltip {
background-color: #EEE;
border: solid 2px #999;
color: #333;
cursor: auto;
left: 50%;
margin-left: -50px;
margin-left: -100px;
padding: 1px;
position: absolute;
top: 120%;
top: 100%;
visibility: hidden;
width: 100px;
width: 200px;
z-index: 1;
}
@ -86,13 +123,31 @@ div.content {
</head>
<body>
<h1>Non-nullable fix instrumentation report</h1>
<p><em>Well-written introduction to this report.</em></p>
{{# units }}
<h2>{{ path }}</h2>
<div class="content">
{{{ content }}}
</div> {{! content }}
{{/ units }}
</body>
</html>
''');
<p><em>Well-written introduction to this report.</em></p>'''
' {{# units }}'
' <h2>{{{ path }}}</h2>'
' <div class="content highlighting">'
'{{! These regions are written out, unmodified, as they need to be found }}'
'{{! in one simple text string for highlight.js to hightlight them. }}'
'{{# regions }}'
'{{ content }}'
'{{/ regions }}'
' <div class="regions">'
'{{! The regions are then printed again, overlaying the first copy of the }}'
'{{! content, to provide tooltips for modified regions. }}'
'{{# regions }}'
'{{^ modified }}{{ content }}{{/ modified }}'
'{{# modified }}<span class="region">{{ content }}'
'<span class="tooltip">{{explanation}}</span></span>{{/ modified }}'
'{{/ regions }}'
'</div></div>'
' {{/ units }}'
' <script lang="javascript">'
'document.addEventListener("DOMContentLoaded", (event) => {'
' document.querySelectorAll(".highlighting").forEach((block) => {'
' hljs.highlightBlock(block);'
' });'
'});'
' </script>'
' </body>'
'</html>');

View file

@ -17,6 +17,41 @@ main() {
@reflectiveTest
class InstrumentationRendererTest extends AbstractAnalysisTest {
test_outputContainsEachPath() async {
LibraryInfo info = LibraryInfo([
UnitInfo('/lib/a.dart', 'int? a = null;',
[RegionInfo(3, 1, 'null was assigned')]),
UnitInfo('/lib/part1.dart', 'int? b = null;',
[RegionInfo(3, 1, 'null was assigned')]),
UnitInfo('/lib/part2.dart', 'int? c = null;',
[RegionInfo(3, 1, 'null was assigned')]),
]);
String output = InstrumentationRenderer(info).render();
expect(output, contains('<h2>/lib/a.dart</h2>'));
expect(output, contains('<h2>/lib/part1.dart</h2>'));
expect(output, contains('<h2>/lib/part2.dart</h2>'));
}
test_outputContainsEscapedHtml() async {
LibraryInfo info = LibraryInfo([
UnitInfo('/lib/a.dart', 'List<String>? a = null;',
[RegionInfo(12, 1, 'null was assigned')]),
]);
String output = InstrumentationRenderer(info).render();
expect(
output,
contains('List&lt;String&gt;<span class="region">?'
'<span class="tooltip">null was assigned</span></span> a = null;'));
}
test_outputContainsEscapedHtml_ampersand() async {
LibraryInfo info = LibraryInfo([
UnitInfo('/lib/a.dart', 'bool a = true && false;', []),
]);
String output = InstrumentationRenderer(info).render();
expect(output, contains('bool a = true &amp;&amp; false;'));
}
test_outputContainsModifiedAndUnmodifiedRegions() async {
LibraryInfo info = LibraryInfo([
UnitInfo('/lib/a.dart', 'int? a = null;',
@ -28,19 +63,4 @@ class InstrumentationRendererTest extends AbstractAnalysisTest {
contains('int<span class="region">?'
'<span class="tooltip">null was assigned</span></span> a = null;'));
}
test_outputContainsEachPath() async {
LibraryInfo info = LibraryInfo([
UnitInfo('/lib/a.dart', 'int? a = null;',
[RegionInfo(3, 1, 'null was assigned')]),
UnitInfo('/lib/part1.dart', 'int? b = null;',
[RegionInfo(3, 1, 'null was assigned')]),
UnitInfo('/lib/part2.dart', 'int? c = null;',
[RegionInfo(3, 1, 'null was assigned')]),
]);
String output = InstrumentationRenderer(info).render();
expect(output, contains('<h2>&#x2F;lib&#x2F;a.dart</h2>'));
expect(output, contains('<h2>&#x2F;lib&#x2F;part1.dart</h2>'));
expect(output, contains('<h2>&#x2F;lib&#x2F;part2.dart</h2>'));
}
}