adding image and icon support for markdownview.ts

Markdown image example:
https://i.imgur.com/NDgsEA4.png

MarkdownImagesMap entries
```
[
  'settingIcon',
 {
  src: 'Images/settings-gear.svg',
  isIcon: true,
  width: '20px',
  height: '20px',
  color: 'var(--color-primary)'
 }
],
[
  'lighthouseLogo',
 {
  src: 'Images/lighthouse_logo.svg',
  isIcon: false,
 }
]
```

Markdown
```
Following is settings icon ![phone icon](settingIcon)
Image in markdown can be done like this ![alt title](lighthouseLogo)
```
Bug:1154783
Change-Id: Ia13e5d2c40de3c457429d7d550dbb326511873c1
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2587634
Commit-Queue: Chait Pinnamaneni <cpinnamaneni@microsoft.com>
Reviewed-by: Jack Franklin <jacktfranklin@chromium.org>
Reviewed-by: Simon Zünd <szuend@chromium.org>
Reviewed-by: Patrick Brosset <patrick.brosset@microsoft.com>
diff --git a/all_devtools_modules.gni b/all_devtools_modules.gni
index 7bd1e04..820ee00 100644
--- a/all_devtools_modules.gni
+++ b/all_devtools_modules.gni
@@ -250,6 +250,8 @@
   "issues/IssueRevealer.js",
   "issues/IssueSurveyLink.js",
   "issues/IssuesPane.js",
+  "issues/MarkdownImage.js",
+  "issues/MarkdownImagesMap.js",
   "issues/MarkdownIssueDescription.js",
   "issues/MarkdownLink.js",
   "issues/MarkdownLinksMap.js",
diff --git a/devtools_grd_files.gni b/devtools_grd_files.gni
index ace8be8..6ffea08 100644
--- a/devtools_grd_files.gni
+++ b/devtools_grd_files.gni
@@ -643,6 +643,8 @@
   "front_end/issues/IssueRevealer.js",
   "front_end/issues/IssueSurveyLink.js",
   "front_end/issues/IssuesPane.js",
+  "front_end/issues/MarkdownImage.js",
+  "front_end/issues/MarkdownImagesMap.js",
   "front_end/issues/MarkdownIssueDescription.js",
   "front_end/issues/MarkdownLink.js",
   "front_end/issues/MarkdownLinksMap.js",
diff --git a/front_end/issues/BUILD.gn b/front_end/issues/BUILD.gn
index b8c83b3..6aadf1f 100644
--- a/front_end/issues/BUILD.gn
+++ b/front_end/issues/BUILD.gn
@@ -15,6 +15,8 @@
     "IssueRevealer.js",
     "IssueSurveyLink.ts",
     "IssuesPane.js",
+    "MarkdownImage.ts",
+    "MarkdownImagesMap.ts",
     "MarkdownIssueDescription.ts",
     "MarkdownLink.ts",
     "MarkdownLinksMap.ts",
diff --git a/front_end/issues/MarkdownImage.ts b/front_end/issues/MarkdownImage.ts
new file mode 100644
index 0000000..8c91f76
--- /dev/null
+++ b/front_end/issues/MarkdownImage.ts
@@ -0,0 +1,78 @@
+// Copyright (c) 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+import * as LitHtml from '../third_party/lit-html/lit-html.js';
+import * as Components from '../ui/components/components.js';
+
+import {getMarkdownImage, ImageData} from './MarkdownImagesMap.js';
+
+export interface MarkdownImageData {
+  key: string;
+  title: string;
+}
+
+/**
+ * Component to render images from parsed markdown.
+ * Parsed images from markdown are not directly rendered, instead they have to be added to the MarkdownImagesMap.ts.
+ * This makes sure that all icons/images are accounted for in markdown.
+ */
+export class MarkdownImage extends HTMLElement {
+  private readonly shadow = this.attachShadow({mode: 'open'});
+  private imageData?: ImageData;
+  private imageTitle?: string;
+
+  constructor() {
+    super();
+  }
+
+  set data(data: MarkdownImageData) {
+    const {key, title} = data;
+    const markdownImage = getMarkdownImage(key);
+    this.imageData = markdownImage;
+    this.imageTitle = title;
+    this.render();
+  }
+
+  private getIconComponent(): LitHtml.TemplateResult {
+    if (!this.imageData) {
+      return LitHtml.html``;
+    }
+    const {src, color, width = '100%', height = '100%'} = this.imageData;
+    return LitHtml.html`
+      <devtools-icon .data=${{iconPath: src, color, width, height} as Components.Icon.IconData}></devtools-icon>
+    `;
+  }
+
+  private getImageComponent(): LitHtml.TemplateResult {
+    if (!this.imageData) {
+      return LitHtml.html``;
+    }
+    const {src, width = '100%', height = '100%'} = this.imageData;
+    return LitHtml.html`
+      <style>
+        .markdown-image {
+          display: block;
+        }
+      </style>
+      <img class="markdown-image" src=${src} alt=${this.imageTitle} width=${width} height=${height}/>
+    `;
+  }
+
+  private render(): void {
+    if (!this.imageData) {
+      return;
+    }
+    const {isIcon} = this.imageData;
+    const imageComponent = isIcon ? this.getIconComponent() : this.getImageComponent();
+    LitHtml.render(imageComponent, this.shadow);
+  }
+}
+
+customElements.define('devtools-markdown-image', MarkdownImage);
+
+declare global {
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  interface HTMLElementTagNameMap {
+    'devtools-markdown-image': MarkdownImage;
+  }
+}
diff --git a/front_end/issues/MarkdownImagesMap.ts b/front_end/issues/MarkdownImagesMap.ts
new file mode 100644
index 0000000..306acf1
--- /dev/null
+++ b/front_end/issues/MarkdownImagesMap.ts
@@ -0,0 +1,46 @@
+// Copyright (c) 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/*
+ * To use images in markdown, add key in markdownImages with the image data and
+ * use the added key in markdown.
+ * @example markdown
+ * Give feedback by clicking ![Feedback icon](feedbackIcon)
+*/
+export interface ImageData {
+  src: string;
+  isIcon: boolean;
+  color?: string;
+  width?: string;
+  height?: string;
+}
+
+/*
+ * src for image is relative url for image location.
+ * @example icon
+ * [
+ *   'feedbackIcon',
+ *   {
+ *     src: 'Images/feedback_thin_16x16_icon.svg',
+ *     isIcon: true,
+ *     width: '20px',
+ *     height: '20px',
+ *     color: 'var(--color-text-disabled)',
+ *   },
+ * ]
+ *
+*/
+
+// NOTE: This is only exported for tests, and it should not be
+// imported in any component, instead add image data in map and
+// use getMarkdownImage to get the appropriate image data.
+export const markdownImages = new Map<string, ImageData>([]);
+
+export const getMarkdownImage = (key: string): ImageData => {
+  const image = markdownImages.get(key);
+  if (!image) {
+    throw new Error(`Markdown image with key '${key}' is not available, please check MarkdownImagesMap.ts`);
+  }
+  return image;
+};
diff --git a/front_end/issues/MarkdownLink.ts b/front_end/issues/MarkdownLink.ts
index 5dd0fbb..13cdb9f 100644
--- a/front_end/issues/MarkdownLink.ts
+++ b/front_end/issues/MarkdownLink.ts
@@ -32,7 +32,7 @@
     const output = LitHtml.html`
       <style>
         .devtools-link {
-          color: var(--color-primary);
+          color: var(--color-link);
           text-decoration: none;
         }
 
diff --git a/front_end/issues/MarkdownView.ts b/front_end/issues/MarkdownView.ts
index 1eaa067..977ffc0 100644
--- a/front_end/issues/MarkdownView.ts
+++ b/front_end/issues/MarkdownView.ts
@@ -4,6 +4,7 @@
 
 import * as LitHtml from '../third_party/lit-html/lit-html.js';
 
+import {MarkdownImageData} from './MarkdownImage.js';
 import {MarkdownLinkData} from './MarkdownLink.js';
 
 const html = LitHtml.html;
@@ -137,6 +138,11 @@
     (token): LitHtml.TemplateResult => html`<devtools-markdown-link .data=${
         {key: token.href, title: token.text} as MarkdownLinkData}></devtools-markdown-link>`,
   ],
+  [
+    'image',
+    (token): LitHtml.TemplateResult => html`<devtools-markdown-image .data=${
+        {key: token.href, title: token.text} as MarkdownImageData}></devtools-markdown-image>`,
+  ],
 ]);
 
 // TODO(crbug.com/1108699): Fix types when they are available.
diff --git a/front_end/issues/issues.ts b/front_end/issues/issues.ts
index 3c65865..d783132 100644
--- a/front_end/issues/issues.ts
+++ b/front_end/issues/issues.ts
@@ -7,6 +7,8 @@
 import * as IssueRevealer from './IssueRevealer.js';
 import * as IssuesPane from './IssuesPane.js';
 import * as IssueSurveyLink from './IssueSurveyLink.js';
+import * as MarkdownImage from './MarkdownImage.js';
+import * as MarkdownImagesMap from './MarkdownImagesMap.js';
 import * as MarkdownIssueDescription from './MarkdownIssueDescription.js';
 import * as MarkdownLink from './MarkdownLink.js';
 import * as MarkdownLinksMap from './MarkdownLinksMap.js';
@@ -18,6 +20,8 @@
   IssueRevealer,
   IssuesPane,
   IssueSurveyLink,
+  MarkdownImage,
+  MarkdownImagesMap,
   MarkdownIssueDescription,
   MarkdownLink,
   MarkdownLinksMap,
diff --git a/test/unittests/front_end/issues/BUILD.gn b/test/unittests/front_end/issues/BUILD.gn
index 24cee29..4bf5352 100644
--- a/test/unittests/front_end/issues/BUILD.gn
+++ b/test/unittests/front_end/issues/BUILD.gn
@@ -5,7 +5,9 @@
   sources = [
     "IssueAggregator_test.ts",
     "IssueSurveyLink_test.ts",
+    "MarkdownImage_test.ts",
     "MarkdownIssueDescription_test.ts",
+    "MarkdownLink_test.ts",
     "MarkdownView_test.ts",
   ]
 
@@ -14,6 +16,7 @@
     "../../../../front_end/issues:bundle",
     "../../../../front_end/sdk:bundle",
     "../../../../front_end/third_party/marked:bundle",
+    "../../../../front_end/ui/components:bundle",
     "../helpers",
     "../sdk",
   ]
diff --git a/test/unittests/front_end/issues/MarkdownImage_test.ts b/test/unittests/front_end/issues/MarkdownImage_test.ts
new file mode 100644
index 0000000..dc9fb4b
--- /dev/null
+++ b/test/unittests/front_end/issues/MarkdownImage_test.ts
@@ -0,0 +1,49 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import type * as IssuesModule from '../../../../front_end/issues/issues.js';
+import {assertShadowRoot, renderElementIntoDOM, getElementWithinComponent} from '../helpers/DOMHelpers.js';
+import * as Components from '../../../../front_end/ui/components/components.js';
+
+const {assert} = chai;
+
+describe('MarkdownImage', () => {
+  let Issues: typeof IssuesModule;
+  const imageSource = 'Images/lighthouse_logo.svg';
+  before(async () => {
+    Issues = await import('../../../../front_end/issues/issues.js');
+    Issues.MarkdownImagesMap.markdownImages.set('test-icon', {
+      src: 'Images/feedback_thin_16x16_icon.svg',
+      isIcon: true,
+    });
+    Issues.MarkdownImagesMap.markdownImages.set('test-image', {
+      src: imageSource,
+      width: '100px',
+      height: '100px',
+      isIcon: false,
+    });
+  });
+  it('renders icon correctly', () => {
+    const component = new Issues.MarkdownImage.MarkdownImage();
+    component.data = {key: 'test-icon', title: 'Test icon'};
+    renderElementIntoDOM(component);
+
+    assertShadowRoot(component.shadowRoot);
+    assert.isNotNull(getElementWithinComponent(component, 'devtools-icon', Components.Icon.Icon));
+  });
+  it('renders image correctly', () => {
+    const component = new Issues.MarkdownImage.MarkdownImage();
+    const markdownImageTitle = 'Test image';
+    const markdownImageKey = 'test-image';
+    component.data = {key: markdownImageKey, title: markdownImageTitle};
+    renderElementIntoDOM(component);
+    assertShadowRoot(component.shadowRoot);
+    const imageComponent = getElementWithinComponent(component, 'img', HTMLImageElement);
+    assert.isNotNull(imageComponent);
+    assert.include(imageComponent.src, imageSource);
+    assert.strictEqual(imageComponent.alt, markdownImageTitle);
+    assert.strictEqual(imageComponent.width, 100);
+    assert.strictEqual(imageComponent.height, 100);
+  });
+});
diff --git a/test/unittests/front_end/issues/MarkdownLink_test.ts b/test/unittests/front_end/issues/MarkdownLink_test.ts
new file mode 100644
index 0000000..4acacad
--- /dev/null
+++ b/test/unittests/front_end/issues/MarkdownLink_test.ts
@@ -0,0 +1,26 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import type * as IssuesModule from '../../../../front_end/issues/issues.js';
+import {assertShadowRoot, renderElementIntoDOM, getElementWithinComponent} from '../helpers/DOMHelpers.js';
+
+const {assert} = chai;
+
+describe('MarkdownLink', () => {
+  let Issues: typeof IssuesModule;
+  before(async () => {
+    Issues = await import('../../../../front_end/issues/issues.js');
+    Issues.MarkdownLinksMap.markdownLinks.set('test-link', 'http://exampleLink');
+  });
+  it('renders link correctly', () => {
+    const component = new Issues.MarkdownLink.MarkdownLink();
+    component.data = {key: 'test-link', title: 'Test link'};
+    renderElementIntoDOM(component);
+    assertShadowRoot(component.shadowRoot);
+    const linkComponent = getElementWithinComponent(component, 'a', HTMLAnchorElement);
+    assert.isNotNull(linkComponent);
+    assert.strictEqual(linkComponent.textContent, 'Test link');
+    assert.strictEqual(linkComponent.href, 'http://examplelink/');
+  });
+});
diff --git a/test/unittests/front_end/issues/MarkdownView_test.ts b/test/unittests/front_end/issues/MarkdownView_test.ts
index 9c0c522..d2c5987 100644
--- a/test/unittests/front_end/issues/MarkdownView_test.ts
+++ b/test/unittests/front_end/issues/MarkdownView_test.ts
@@ -73,6 +73,30 @@
     it('throws an error if invalid link key is provided', () => {
       assert.throws(() => Issues.MarkdownLinksMap.getMarkdownLink('testErrorLink'));
     });
+
+    it('renders icon with valid key', () => {
+      Issues.MarkdownImagesMap.markdownImages.set('testExampleImage', {
+        src: 'largeicon-phone',
+        isIcon: true,
+      });
+      const renderResult = Issues.MarkdownView.renderToken({type: 'image', text: 'phone', href: 'testExampleImage'});
+      assert.deepStrictEqual(
+          renderResult.strings.raw, ['<devtools-markdown-image .data=', '></devtools-markdown-image>']);
+    });
+
+    it('renders image with valid key', () => {
+      Issues.MarkdownImagesMap.markdownImages.set('exampleImage', {
+        src: 'Images/phone-logo.png',
+        isIcon: false,
+      });
+      const renderResult = Issues.MarkdownView.renderToken({type: 'image', text: 'phone', href: 'exampleImage'});
+      assert.deepStrictEqual(
+          renderResult.strings.raw, ['<devtools-markdown-image .data=', '></devtools-markdown-image>']);
+    });
+
+    it('throws an error if invalid image key is provided', () => {
+      assert.throws(() => Issues.MarkdownImagesMap.getMarkdownImage('testErrorImageLink'));
+    });
   });
 
   const paragraphText =