1// Copyright 2020 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5import * as LitHtml from '../third_party/lit-html/lit-html.js'; 6 7const html = LitHtml.html; 8const render = LitHtml.render; 9 10export interface MarkdownViewData { 11 tokens: Object[]; 12} 13 14export class MarkdownView extends HTMLElement { 15 private readonly shadow = this.attachShadow({mode: 'open'}); 16 17 private tokenData: ReadonlyArray<Object> = []; 18 19 set data(data: MarkdownViewData) { 20 this.tokenData = data.tokens; 21 this.update(); 22 } 23 24 private update() { 25 this.render(); 26 } 27 28 private render() { 29 // Disabled until https://crbug.com/1079231 is fixed. 30 // clang-format off 31 render(html` 32 <style> 33 .message { 34 line-height: 20px; 35 font-size: 14px; 36 color: var(--issue-gray); 37 margin-bottom: 4px; 38 user-select: text; 39 } 40 41 .message p { 42 margin-bottom: 16px; 43 margin-block-start: 2px; 44 } 45 46 .message ul { 47 list-style-type: none; 48 list-style-position: inside; 49 padding-inline-start: 0; 50 } 51 52 .message li { 53 margin-top: 8px; 54 display: list-item; 55 } 56 57 .message li::before { 58 content: "→"; 59 -webkit-mask-image: none; 60 padding-right: 5px; 61 position: relative; 62 top: -1px; 63 } 64 65 .message code { 66 color: var(--issue-black); 67 font-size: 12px; 68 user-select: text; 69 cursor: text; 70 background: var(--issue-code); 71 } 72 </style> 73 <div class='message'> 74 ${this.tokenData.map(renderToken)} 75 </div> 76 `, this.shadow); 77 // clang-format on 78 } 79} 80 81customElements.define('devtools-markdown-view', MarkdownView); 82 83declare global { 84 interface HTMLElementTagNameMap { 85 'devtools-markdown-view': MarkdownView; 86 } 87} 88 89// TODO(crbug.com/1108699): Fix types when they are available. 90// eslint-disable-next-line @typescript-eslint/no-explicit-any 91const renderChildTokens = (token: any) => { 92 return token.tokens.map(renderToken); 93}; 94 95const unescape = (text: string): string => { 96 // Unescape will get rid of the escaping done by Marked to avoid double escaping due to escaping it also with Lit-html 97 // Table taken from: front_end/third_party/marked/package/src/helpers.js 98 /** @type {Map<string,string>} */ 99 const escapeReplacements = new Map<string, string>([ 100 ['&', '&'], 101 ['<', '<'], 102 ['>', '>'], 103 ['"', '"'], 104 [''', '\''], 105 ]); 106 return text.replace(/&(amp|lt|gt|quot|#39);/g, (matchedString: string) => { 107 const replacement = escapeReplacements.get(matchedString); 108 return replacement ? replacement : matchedString; 109 }); 110}; 111// TODO(crbug.com/1108699): Fix types when they are available. 112// eslint-disable-next-line @typescript-eslint/no-explicit-any 113const renderText = (token: any) => { 114 if (token.tokens && token.tokens.length > 0) { 115 return html`${renderChildTokens(token)}`; 116 } 117 // Due to unescaping, unescaped html entities (see escapeReplacements' keys) will be rendered 118 // as their corresponding symbol while the rest will be rendered as verbatim. 119 // Marked's escape function can be found in front_end/third_party/marked/package/src/helpers.js 120 return html`${unescape(token.text)}`; 121}; 122 123// TODO(crbug.com/1108699): Fix types when they are available. 124// eslint-disable-next-line @typescript-eslint/no-explicit-any 125const tokenRenderers = new Map<string, (token: any) => LitHtml.TemplateResult>([ 126 ['paragraph', token => html`<p>${renderChildTokens(token)}</p>`], 127 ['list', token => html`<ul>${token.items.map(renderToken)}</ul>`], 128 ['list_item', token => html`<li>${renderChildTokens(token)}</li>`], 129 ['text', renderText], 130 ['codespan', token => html`<code>${unescape(token.text)}</code>`], 131 ['space', () => html``], 132]); 133 134// TODO(crbug.com/1108699): Fix types when they are available. 135// eslint-disable-next-line @typescript-eslint/no-explicit-any 136export const renderToken = (token: any) => { 137 const renderFn = tokenRenderers.get(token.type); 138 if (!renderFn) { 139 throw new Error(`Markdown token type '${token.type}' not supported.`); 140 } 141 return renderFn(token); 142}; 143