[results feed] Add links to Github issues in comments.

Support links like #20497 or "co19 #479" in comments.
Also support '\n' for line breaks in comments.

Change-Id: If86d3da68ea5ab9815149ee4fa73fd28c2afa4d6
Reviewed-on: https://dart-review.googlesource.com/c/dart_ci/+/133067
Reviewed-by: Jonas Termansen <sortie@google.com>
diff --git a/results_feed/lib/src/components/blamelist_component.dart b/results_feed/lib/src/components/blamelist_component.dart
index a1de2ab..2368b80 100644
--- a/results_feed/lib/src/components/blamelist_component.dart
+++ b/results_feed/lib/src/components/blamelist_component.dart
@@ -1,5 +1,6 @@
 import 'package:angular/angular.dart';
 import 'package:angular_components/laminate/enums/alignment.dart';
+import 'package:angular_components/simple_html/simple_html.dart';
 import 'package:angular_forms/angular_forms.dart';
 
 import '../formatting.dart';
@@ -8,7 +9,12 @@
 
 @Component(
     selector: 'blamelist-panel',
-    directives: [coreDirectives, formDirectives, RelativePosition],
+    directives: [
+      coreDirectives,
+      formDirectives,
+      RelativePosition,
+      SimpleHtmlComponent,
+    ],
     templateUrl: 'blamelist_component.html',
     styleUrls: ['blamelist.css'],
     exports: [formattedDate, formattedEmail])
diff --git a/results_feed/lib/src/components/blamelist_component.html b/results_feed/lib/src/components/blamelist_component.html
index b900c21..22d6c6e 100644
--- a/results_feed/lib/src/components/blamelist_component.html
+++ b/results_feed/lib/src/components/blamelist_component.html
@@ -16,6 +16,6 @@
     <b>{{c.approvedText()}}</b>
     <span class="nowrap">{{formattedDate(c.created)}}</span>
     {{formattedEmail(c.author)}}<br>
-    <div class="comment-body">{{c.comment}}</div>
+    <simple-html class="comment-body" [contents]="c.commentHtml"></simple-html>
   </div>
 </div>
diff --git a/results_feed/lib/src/components/blamelist_picker.dart b/results_feed/lib/src/components/blamelist_picker.dart
index a741ff5..9b4febc 100644
--- a/results_feed/lib/src/components/blamelist_picker.dart
+++ b/results_feed/lib/src/components/blamelist_picker.dart
@@ -6,9 +6,10 @@
 
 import 'package:angular/angular.dart';
 import 'package:angular_components/angular_components.dart';
-import 'package:angular_forms/angular_forms.dart' show formDirectives;
 import 'package:angular_components/material_radio/material_radio.dart';
 import 'package:angular_components/material_radio/material_radio_group.dart';
+import 'package:angular_components/simple_html/simple_html.dart';
+import 'package:angular_forms/angular_forms.dart' show formDirectives;
 
 import '../formatting.dart';
 import '../model/comment.dart';
@@ -21,6 +22,7 @@
       formDirectives,
       MaterialRadioComponent,
       MaterialRadioGroupComponent,
+      SimpleHtmlComponent,
     ],
     templateUrl: 'blamelist_picker.html',
     styleUrls: ([
diff --git a/results_feed/lib/src/components/blamelist_picker.html b/results_feed/lib/src/components/blamelist_picker.html
index b953576..33fc389 100644
--- a/results_feed/lib/src/components/blamelist_picker.html
+++ b/results_feed/lib/src/components/blamelist_picker.html
@@ -14,6 +14,6 @@
     <b>{{c.approvedText()}}</b>
     <span class="nowrap">{{formattedDate(c.created)}}</span><br>
     {{formattedEmail(c.author)}}
-    <div class="comment-body">{{c.comment}}</div>
+    <simple-html class="comment-body" [contents]="c.commentHtml"></simple-html>
   </div>
 </material-radio-group>
diff --git a/results_feed/lib/src/components/commit_component.html b/results_feed/lib/src/components/commit_component.html
index d102395..5df426f 100644
--- a/results_feed/lib/src/components/commit_component.html
+++ b/results_feed/lib/src/components/commit_component.html
@@ -40,7 +40,8 @@
         failuresOnly></results-selector-panel>
   </div>
   <div *ngIf="approving" class="commit-bottom">
-    Comment:<br>
+    Comment: (type #4567 for GitHub SDK issue 4567,
+    or co19 #123 for a co19 issue. Use \n for newline.)<br>
     <material-input
         multiline
         label="Comment"
diff --git a/results_feed/lib/src/components/try_results_component.dart b/results_feed/lib/src/components/try_results_component.dart
index e4858fc..39796f3 100644
--- a/results_feed/lib/src/components/try_results_component.dart
+++ b/results_feed/lib/src/components/try_results_component.dart
@@ -9,6 +9,7 @@
 import 'package:angular_components/material_button/material_button.dart';
 import 'package:angular_components/material_input/material_input.dart';
 import 'package:angular_components/material_input/material_input_multiline.dart';
+import 'package:angular_components/simple_html/simple_html.dart';
 import 'package:angular_forms/angular_forms.dart' show formDirectives;
 import 'package:angular_router/angular_router.dart';
 
@@ -28,7 +29,8 @@
       MaterialButtonComponent,
       MaterialMultilineInputComponent,
       ResultsPanel,
-      ResultsSelectorPanel
+      ResultsSelectorPanel,
+      SimpleHtmlComponent,
     ],
     providers: [
       ClassProvider(TryDataService),
diff --git a/results_feed/lib/src/components/try_results_component.html b/results_feed/lib/src/components/try_results_component.html
index d1baf68..1b6dd3a 100644
--- a/results_feed/lib/src/components/try_results_component.html
+++ b/results_feed/lib/src/components/try_results_component.html
@@ -21,10 +21,12 @@
     <b>{{c.approvedText()}}</b>
     <span class="nowrap">{{formattedDate(c.created)}}</span>
     {{formattedEmail(c.author)}}<br>
-    <div style="padding-left: 32px">{{c.comment}}</div>
+    <simple-html style="padding-left: 32px" [contents]="c.commentHtml">
+    </simple-html>
   </div>
   <div *ngIf="approving">
-    Comment:<br>
+    Comment: (type #4567 for GitHub SDK issue 4567,
+    or co19 #123 for a co19 issue. Use \n for newline.)<br>
     <material-input
         multiline
         style="width: 80%"
diff --git a/results_feed/lib/src/model/comment.dart b/results_feed/lib/src/model/comment.dart
index 40434b5..265cfd0 100644
--- a/results_feed/lib/src/model/comment.dart
+++ b/results_feed/lib/src/model/comment.dart
@@ -19,6 +19,7 @@
   final int pinnedIndex;
   final int review;
   final int patchset;
+  String commentHtml;
 
   Comment(
       this.id,
@@ -36,7 +37,9 @@
       this.review,
       this.patchset)
       : results = List<String>.from(_results),
-        tryResults = List<String>.from(_tryResults);
+        tryResults = List<String>.from(_tryResults) {
+    commentHtml = createCommentHtml();
+  }
 
   Comment.fromDocument(DocumentSnapshot document)
       : id = document.id,
@@ -52,10 +55,30 @@
         blamelistEndIndex = document.get('blamelist_end_index'),
         pinnedIndex = document.get('pinned_index'),
         review = document.get('review'),
-        patchset = document.get('patchset');
+        patchset = document.get('patchset') {
+    commentHtml = createCommentHtml();
+  }
 
   String approvedText() =>
       (approved == null) ? "" : approved ? "approved" : "disapproved";
 
+  static const repositories = ['sdk', 'co19'];
+  // Matches a repository or nothing, followed by #[digits][word break].
+  static final issueMatcher =
+      RegExp('(${repositories.join("|")})?\\s*#(\\d+)\\b');
+
+  String createCommentHtml() {
+    final result = comment
+        ?.replaceAll('<', '&lt;')
+        ?.replaceAll('\\n', '<br>')
+        ?.replaceAllMapped(
+            issueMatcher,
+            (match) =>
+                '<a target="_blank" rel="noopener" href="https://github.com/'
+                'dart-lang/${match[1] ?? "sdk"}/issues/${match[2]}">'
+                '${match[0]}</a>');
+    return result;
+  }
+
   int compareTo(Object other) => created.compareTo((other as Comment).created);
 }
diff --git a/results_feed/web/main.dart b/results_feed/web/main.dart
index 9329437..0106553 100644
--- a/results_feed/web/main.dart
+++ b/results_feed/web/main.dart
@@ -3,6 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:angular/angular.dart';
+import 'package:angular_components/simple_html/simple_html.dart'
+    show simpleHtmlUriWhitelist;
 import 'package:angular_router/angular_router.dart';
 import 'package:dart_results_feed/src/services/firestore_service.dart';
 import 'package:dart_results_feed/src/components/routing_wrapper_component.template.dart'
@@ -14,7 +16,16 @@
 // Use for deploying on staging website:
 // @GenerateInjector([ClassProvider(FirestoreService, useClass: StagingFirestoreService), ...routerProviders])
 
-@GenerateInjector([ClassProvider(FirestoreService), ...routerProviders])
+// Allow links from comments to GitHub issues in the dart-lang organization.
+List<Uri> getUriWhitelist() => List.unmodifiable(<Uri>[
+      Uri.https('github.com', 'dart-lang/'),
+    ]);
+
+@GenerateInjector([
+  ClassProvider(FirestoreService),
+  FactoryProvider.forToken(simpleHtmlUriWhitelist, getUriWhitelist),
+  routerProviders
+])
 final InjectorFactory injector = self.injector$Injector;
 
 void main() {