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  Image in markdown can be done like this  ``` 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  +*/ +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 =