blob: 8a22b0d13048642a990791072515ebfd41c4243a [file] [log] [blame]
Adrià Vilanova Martínezac4a6442022-05-15 19:05:13 +02001import { marked } from 'marked';
Copybara854996b2021-09-07 19:36:02 +00002import DOMPurify from 'dompurify';
3
Adrià Vilanova Martínez2d5457a2022-01-13 13:25:39 +01004const EMAIL_REGEX = /^mailto:[-a-zA-Z0-9!#$%&'*+\/=?^_`{|}~]+(?:[.][-a-zA-Z0-9!#$%&'*+\/=?^_`{|}~]+)*@(?:(?:[0-9a-zA-Z](?:[-]*[0-9a-zA-Z]+)*)(?:\.[0-9a-zA-Z](?:[-]*[0-9a-zA-Z]+)*)*)\.(?:[a-zA-Z]{2,9})$/;
5const MONORAIL_USER_REGEX = /\/u\/[-a-zA-Z0-9!#$%&'*+\/=?^_`{|}~]+(?:[.][-a-zA-Z0-9!#$%&'*+\/=?^_`{|}~]+)*@(?:(?:[0-9a-zA-Z](?:[-]*[0-9a-zA-Z]+)*)(?:\.[0-9a-zA-Z](?:[-]*[0-9a-zA-Z]+)*)*)\.(?:[a-zA-Z]{2,9})$/;
Copybara854996b2021-09-07 19:36:02 +00006
7/** @type {Set} Authors whose comments will not be rendered as Markdown. */
8const BLOCKLIST = new Set(['sheriffbot@sheriffbot-1182.iam.gserviceaccount.com',
9 'sheriff-o-matic@appspot.gserviceaccount.com',
10 'sheriff-o-matic-staging@appspot.gserviceaccount.com',
11 'bugdroid1@chromium.org',
12 'bugdroid@chops-service-accounts.iam.gserviceaccount.com',
13 'gitwatcher-staging.google.com@appspot.gserviceaccount.com',
Adrià Vilanova Martíneze3e91652021-09-08 17:37:29 +020014 'gitwatcher.google.com@appspot.gserviceaccount.com',
15 'gitwatcher@avm99963-bugs.iam.gserviceaccount.com',
16 'Git Watcher',]);
Copybara854996b2021-09-07 19:36:02 +000017
18/**
19 * Determines whether content should be rendered as Markdown.
20 * @param {string} options.project Project this content belongs to.
21 * @param {number} options.author User who authored this content.
22 * @param {boolean} options.enabled Per-issue override to force Markdown.
23 * @param {Array<string>} options.availableProjects List of opted in projects.
24 * @return {boolean} Whether this content should be rendered as Markdown.
25 */
26export const shouldRenderMarkdown = ({
Adrià Vilanova Martínezd5550d42022-01-13 13:34:38 +010027 project, author, enabled = true
Copybara854996b2021-09-07 19:36:02 +000028} = {}) => {
Adrià Vilanova Martínez535e7312021-10-17 00:48:12 +020029 if (BLOCKLIST.has(author)) {
Copybara854996b2021-09-07 19:36:02 +000030 return false;
31 } else if (!enabled) {
32 return false;
Copybara854996b2021-09-07 19:36:02 +000033 }
Adrià Vilanova Martínezd5550d42022-01-13 13:34:38 +010034 return true;
Copybara854996b2021-09-07 19:36:02 +000035};
36
37/** @const {Object} Options for DOMPurify sanitizer */
38const SANITIZE_OPTIONS = Object.freeze({
39 RETURN_TRUSTED_TYPE: true,
40 FORBID_TAGS: ['style'],
Adrià Vilanova Martínez9f9ade52022-10-10 23:20:11 +020041 FORBID_ATTR: ['style', 'autoplay', 'src'],
Copybara854996b2021-09-07 19:36:02 +000042});
43
44/**
45 * Replaces bold HTML tags in comment with Markdown equivalent.
46 * @param {string} raw Comment string as stored in database.
47 * @return {string} Comment string after b tags are placed by Markdown bolding.
48 */
49const replaceBoldTag = (raw) => {
50 return raw.replace(/<b>|<\/b>/g, '**');
51};
52
Copybara854996b2021-09-07 19:36:02 +000053/**
54* Checks to see if input string is a valid HTTP link.
55 * @param {string} string
56 * @return {boolean} Whether input string is a valid HTTP(s) link.
57 */
58const isValidHttpUrl = (string) => {
59 let url;
60
61 try {
62 url = new URL(string);
63 } catch (_exception) {
64 return false;
65 }
66
67 return url.protocol === 'http:' || url.protocol === 'https:';
68};
69
70/**
Adrià Vilanova Martínez2d5457a2022-01-13 13:25:39 +010071* Checks to see if input string matches a href generated by Monorail's autolinking code.
72 * @param {string} string
73 * @return {boolean} Whether input string is an email address.
74 */
75const isEmailLink = (string) => {
76 return EMAIL_REGEX.test(string) || MONORAIL_USER_REGEX.test(string)
77}
78
79/**
Copybara854996b2021-09-07 19:36:02 +000080 * Renderer option for Marked.
81 * See https://marked.js.org/using_pro#renderer on how to use renderer.
82 * @type {Object}
83 */
84const renderer = {
Copybara854996b2021-09-07 19:36:02 +000085 link(href, title, text) {
86 // Overrides default link rendering by adding icon and destination on hover.
87 // TODO(crbug.com/monorail/9316): Add shared-styles/MD_STYLES to all
88 // components that consume the markdown renderer.
89 let linkIcon;
90 let tooltipText;
Adrià Vilanova Martínez2d5457a2022-01-13 13:25:39 +010091 if (isValidHttpUrl(href) || isEmailLink(href)) {
Copybara854996b2021-09-07 19:36:02 +000092 linkIcon = `<span class="material-icons link">link</span>`;
93 tooltipText = `Link destination: ${href}`;
94 } else {
95 linkIcon = `<span class="material-icons link_off">link_off</span>`;
96 tooltipText = `Link may be malformed: ${href}`;
97 }
98 const tooltip = `<span class="tooltip">${tooltipText}</span>`;
99 return `<span class="annotated-link"><a href=${href} ` +
100 `title=${title ? title : ''}>${linkIcon}${text}</a>${tooltip}</span>`;
101 },
102};
103
104marked.use({renderer, headerIds: false});
105
106/**
107 * Renders Markdown content into HTML.
108 * @param {string} raw Content to be intepretted as Markdown.
109 * @return {string} Rendered content in HTML format.
110 */
111export const renderMarkdown = (raw) => {
112 // TODO(crbug.com/monorail/9310): Add commentReferences, projectName,
113 // and revisionUrlFormat to use in conjunction with Marked's lexer for
114 // autolinking.
115 // TODO(crbug.com/monorail/9310): Integrate autolink
116 const preprocessed = replaceBoldTag(raw);
Adrià Vilanova Martínez9f9ade52022-10-10 23:20:11 +0200117 const converted = marked(preprocessed);
Copybara854996b2021-09-07 19:36:02 +0000118 const sanitized = DOMPurify.sanitize(converted, SANITIZE_OPTIONS);
119 return sanitized.toString();
120};