| import 'package:analysis_server/src/edit/nnbd_migration/migration_info.dart'; |
| import 'package:meta/meta.dart'; |
| import 'package:mustache/mustache.dart' as mustache; |
| import 'package:path/path.dart' as path; |
| |
| /// Instrumentation display output for a library that was migrated to use |
| /// non-nullable types. |
| class InstrumentationRenderer { |
| /// Display information for a library. |
| final LibraryInfo libraryInfo; |
| |
| /// Information for a whole migration, so that libraries can reference each |
| /// other. |
| final MigrationInfo migrationInfo; |
| |
| /// Creates an output object for the given library info. |
| InstrumentationRenderer(this.libraryInfo, this.migrationInfo); |
| |
| /// Builds an HTML view of the instrumentation information in [libraryInfo]. |
| String render() { |
| int previousIndex = 0; |
| Map<String, dynamic> mustacheContext = { |
| 'units': <Map<String, dynamic>>[], |
| 'links': migrationInfo.libraryLinks(libraryInfo), |
| 'highlightJsPath': migrationInfo.highlightJsPath(libraryInfo), |
| 'highlightStylePath': migrationInfo.highlightStylePath(libraryInfo), |
| }; |
| for (var compilationUnit in libraryInfo.units) { |
| // 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. |
| regions.add({ |
| 'modified': false, |
| 'content': |
| compilationUnit.content.substring(previousIndex, region.offset) |
| }); |
| previousIndex = region.offset + region.length; |
| } |
| 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. |
| regions.add({ |
| 'modified': false, |
| 'content': compilationUnit.content.substring(previousIndex) |
| }); |
| } |
| mustacheContext['units'] |
| .add({'path': compilationUnit.path, 'regions': regions}); |
| } |
| return _template.renderString(mustacheContext); |
| } |
| } |
| |
| /// A class storing rendering information for an entire migration report. |
| /// |
| /// This generally provides one [InstrumentationRenderer] (for one library) |
| /// with information about the rest of the libraries represented in the |
| /// instrumentation output. |
| class MigrationInfo { |
| /// The information about the libraries that are are migrated. |
| final List<LibraryInfo> libraries; |
| |
| /// The resource provider's path context. |
| final path.Context pathContext; |
| |
| /// The filesystem root used to create relative paths for each unit. |
| final String includedRoot; |
| |
| MigrationInfo(this.libraries, this.pathContext, this.includedRoot); |
| |
| /// Generate mustache context for library links, for navigation in the |
| /// instrumentation document for [thisLibrary]. |
| List<Map<String, Object>> libraryLinks(LibraryInfo thisLibrary) { |
| return [ |
| for (var library in libraries) |
| { |
| 'name': _computeName(library), |
| 'isLink': library != thisLibrary, |
| if (library != thisLibrary) |
| 'href': _pathTo(library, source: thisLibrary) |
| } |
| ]; |
| } |
| |
| /// Return the path to [library] from [includedRoot], to be used as a display |
| /// name for a library. |
| String _computeName(LibraryInfo library) => |
| pathContext.relative(library.units.first.path, from: includedRoot); |
| |
| /// The path to [target], relative to [from]. |
| String _pathTo(LibraryInfo target, {@required LibraryInfo source}) { |
| assert(target.units.isNotEmpty); |
| assert(source.units.isNotEmpty); |
| String targetPath = |
| pathContext.setExtension(target.units.first.path, '.html'); |
| String sourceDir = pathContext.dirname(source.units.first.path); |
| return pathContext.relative(targetPath, from: sourceDir); |
| } |
| |
| /// The path to the highlight.js script, relative to [libraryInfo]. |
| String highlightJsPath(LibraryInfo libraryInfo) => |
| pathContext.relative(pathContext.join(includedRoot, 'highlight.pack.js'), |
| from: pathContext.dirname(libraryInfo.units.first.path)); |
| |
| /// The path to the highlight.js stylesheet, relative to [libraryInfo]. |
| String highlightStylePath(LibraryInfo libraryInfo) => |
| pathContext.relative(pathContext.join(includedRoot, 'androidstudio.css'), |
| from: pathContext.dirname(libraryInfo.units.first.path)); |
| } |
| |
| /// A mustache template for one library's instrumentation output. |
| mustache.Template _template = mustache.Template(r''' |
| <html> |
| <head> |
| <title>Non-nullable fix instrumentation report</title> |
| <script src="{{ highlightJsPath }}"></script> |
| <link rel="stylesheet" href="{{ highlightStylePath }}"> |
| <style> |
| body { |
| font-family: sans-serif; |
| padding: 1em; |
| } |
| |
| h2 { |
| font-size: 1em; |
| font-weight: bold; |
| } |
| |
| .content { |
| font-family: monospace; |
| position: relative; |
| white-space: pre; |
| } |
| |
| .regions { |
| position: absolute; |
| left: 0.5em; |
| 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. */ |
| 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; |
| font-family: sans-serif; |
| font-size: 0.8em; |
| left: 0; |
| margin-left: 0; |
| padding: 1px; |
| position: absolute; |
| top: 100%; |
| visibility: hidden; |
| white-space: normal; |
| width: 200px; |
| z-index: 1; |
| } |
| |
| .region:hover .tooltip { |
| visibility: visible; |
| } |
| </style> |
| </head> |
| <body> |
| <h1>Non-nullable fix instrumentation report</h1> |
| <p><em>Well-written introduction to this report.</em></p> |
| <div class="navigation"> |
| {{# links }} |
| {{# isLink }}<a href="{{ href }}">{{ name }}</a>{{/ isLink }} |
| {{^ isLink }}{{ name }}{{/ isLink }} |
| <br /> |
| {{/ links }} |
| </div> |
| {{# units }}''' |
| '<h2>{{{ path }}}</h2>' |
| '<div class="content">' |
| '<div class="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>' |
| '<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>' |
| r''' |
| {{/ units }} |
| <script lang="javascript"> |
| document.addEventListener("DOMContentLoaded", (event) => { |
| document.querySelectorAll(".highlighting").forEach((block) => { |
| hljs.highlightBlock(block); |
| }); |
| }); |
| </script> |
| </body> |
| </html>'''); |