blob: a2f511ce5f7d0481424a9f2062eed872d34020b5 [file] [log] [blame]
Copybara854996b2021-09-07 19:36:02 +00001import marked from 'marked';
2import DOMPurify from 'dompurify';
3
4/** @type {Set} Projects that default Markdown rendering to true. */
Adrià Vilanova Martínez89e40882021-09-07 01:29:07 +02005export const DEFAULT_MD_PROJECTS = new Set(['vulnz', 'vulnz-old', 'twpowertools', 'translateselectedtext']);
Copybara854996b2021-09-07 19:36:02 +00006
7/** @type {Set} Projects that allow users to opt into Markdown rendering. */
8export const AVAILABLE_MD_PROJECTS = new Set([...DEFAULT_MD_PROJECTS]);
9
10/** @type {Set} Authors whose comments will not be rendered as Markdown. */
11const BLOCKLIST = new Set(['sheriffbot@sheriffbot-1182.iam.gserviceaccount.com',
12 'sheriff-o-matic@appspot.gserviceaccount.com',
13 'sheriff-o-matic-staging@appspot.gserviceaccount.com',
14 'bugdroid1@chromium.org',
15 'bugdroid@chops-service-accounts.iam.gserviceaccount.com',
16 'gitwatcher-staging.google.com@appspot.gserviceaccount.com',
Adrià Vilanova Martíneze3e91652021-09-08 17:37:29 +020017 'gitwatcher.google.com@appspot.gserviceaccount.com',
18 'gitwatcher@avm99963-bugs.iam.gserviceaccount.com',
19 'Git Watcher',]);
Copybara854996b2021-09-07 19:36:02 +000020
21/**
22 * Determines whether content should be rendered as Markdown.
23 * @param {string} options.project Project this content belongs to.
24 * @param {number} options.author User who authored this content.
25 * @param {boolean} options.enabled Per-issue override to force Markdown.
26 * @param {Array<string>} options.availableProjects List of opted in projects.
27 * @return {boolean} Whether this content should be rendered as Markdown.
28 */
29export const shouldRenderMarkdown = ({
30 project, author, enabled = true, availableProjects = AVAILABLE_MD_PROJECTS
31} = {}) => {
32 if (author in BLOCKLIST) {
33 return false;
34 } else if (!enabled) {
35 return false;
36 } else if (availableProjects.has(project)) {
37 return true;
38 }
39 return false;
40};
41
42/** @const {Object} Options for DOMPurify sanitizer */
43const SANITIZE_OPTIONS = Object.freeze({
44 RETURN_TRUSTED_TYPE: true,
45 FORBID_TAGS: ['style'],
46 FORBID_ATTR: ['style', 'autoplay'],
47});
48
49/**
50 * Replaces bold HTML tags in comment with Markdown equivalent.
51 * @param {string} raw Comment string as stored in database.
52 * @return {string} Comment string after b tags are placed by Markdown bolding.
53 */
54const replaceBoldTag = (raw) => {
55 return raw.replace(/<b>|<\/b>/g, '**');
56};
57
58/** @const {Object} Basic HTML character escape mapping */
59const HTML_ESCAPE_MAP = Object.freeze({
60 '&': '&amp;',
61 '<': '&lt;',
62 '>': '&gt;',
63 '"': '&quot;',
64 '\'': '&#39;',
65 '/': '&#x2F;',
66 '`': '&#x60;',
67 '=': '&#x3D;',
68});
69
70/**
71 * Escapes HTML characters, used to render HTML blocks in Markdown. This
72 * alleviates security flaws but is not the primary security barrier, that is
73 * handled by DOMPurify.
74 * @param {string} text Content that looks to Marked parser to contain HTML.
75 * @return {string} Same text content after escaping HTML characters.
76 */
77const escapeHtml = (text) => {
78 return text.replace(/[&<>"'`=\/]/g, (s) => {
79 return HTML_ESCAPE_MAP[s];
80 });
81};
82
83/**
84* Checks to see if input string is a valid HTTP link.
85 * @param {string} string
86 * @return {boolean} Whether input string is a valid HTTP(s) link.
87 */
88const isValidHttpUrl = (string) => {
89 let url;
90
91 try {
92 url = new URL(string);
93 } catch (_exception) {
94 return false;
95 }
96
97 return url.protocol === 'http:' || url.protocol === 'https:';
98};
99
100/**
101 * Renderer option for Marked.
102 * See https://marked.js.org/using_pro#renderer on how to use renderer.
103 * @type {Object}
104 */
105const renderer = {
106 html(text) {
107 // Do not render HTML, instead escape HTML and render as plaintext.
108 return escapeHtml(text);
109 },
110 link(href, title, text) {
111 // Overrides default link rendering by adding icon and destination on hover.
112 // TODO(crbug.com/monorail/9316): Add shared-styles/MD_STYLES to all
113 // components that consume the markdown renderer.
114 let linkIcon;
115 let tooltipText;
116 if (isValidHttpUrl(href)) {
117 linkIcon = `<span class="material-icons link">link</span>`;
118 tooltipText = `Link destination: ${href}`;
119 } else {
120 linkIcon = `<span class="material-icons link_off">link_off</span>`;
121 tooltipText = `Link may be malformed: ${href}`;
122 }
123 const tooltip = `<span class="tooltip">${tooltipText}</span>`;
124 return `<span class="annotated-link"><a href=${href} ` +
125 `title=${title ? title : ''}>${linkIcon}${text}</a>${tooltip}</span>`;
126 },
127};
128
129marked.use({renderer, headerIds: false});
130
131/**
132 * Renders Markdown content into HTML.
133 * @param {string} raw Content to be intepretted as Markdown.
134 * @return {string} Rendered content in HTML format.
135 */
136export const renderMarkdown = (raw) => {
137 // TODO(crbug.com/monorail/9310): Add commentReferences, projectName,
138 // and revisionUrlFormat to use in conjunction with Marked's lexer for
139 // autolinking.
140 // TODO(crbug.com/monorail/9310): Integrate autolink
141 const preprocessed = replaceBoldTag(raw);
142 const converted = marked(preprocessed);
143 const sanitized = DOMPurify.sanitize(converted, SANITIZE_OPTIONS);
144 return sanitized.toString();
145};