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    ['&amp;', '&'],
101    ['&lt;', '<'],
102    ['&gt;', '>'],
103    ['&quot;', '"'],
104    ['&#39;', '\''],
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