// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. import * as TextFormatting from './text_formatting.jsx'; import * as SyntaxHighlighting from './syntax_highlighting.jsx'; import marked from 'marked'; import katex from 'katex'; function markdownImageLoaded(image) { if (image.hasAttribute('height') && image.attributes.height.value !== 'auto') { const maxHeight = parseInt(global.getComputedStyle(image).maxHeight, 10); if (image.attributes.height.value > maxHeight) { image.style.height = maxHeight + 'px'; image.style.width = ((maxHeight * image.attributes.width.value) / image.attributes.height.value) + 'px'; } else { image.style.height = image.attributes.height.value + 'px'; } } else { image.style.height = 'auto'; } } global.markdownImageLoaded = markdownImageLoaded; class MattermostMarkdownRenderer extends marked.Renderer { constructor(options, formattingOptions = {}) { super(options); this.heading = this.heading.bind(this); this.paragraph = this.paragraph.bind(this); this.text = this.text.bind(this); this.formattingOptions = formattingOptions; } code(code, language) { let usedLanguage = language || ''; usedLanguage = usedLanguage.toLowerCase(); if (usedLanguage === 'tex' || usedLanguage === 'latex') { try { const html = katex.renderToString(code, {throwOnError: false, displayMode: true}); return '
' + html + '
'; } catch (e) { // fall through if latex parsing fails and handle below } } // treat html as xml to prevent injection attacks if (usedLanguage === 'html') { usedLanguage = 'xml'; } let className = 'post-code'; if (!usedLanguage) { className += ' post-code--wrap'; } let header = ''; if (SyntaxHighlighting.canHighlight(usedLanguage)) { header = ( '' + SyntaxHighlighting.getLanguageName(language) + '' ); } // if we have to apply syntax highlighting AND highlighting of search terms, create two copies // of the code block, one with syntax highlighting applied and another with invisible text, but // search term highlighting and overlap them const content = SyntaxHighlighting.highlight(usedLanguage, code); let searchedContent = ''; if (this.formattingOptions.searchPatterns) { const tokens = new Map(); let searched = TextFormatting.sanitizeHtml(code); searched = TextFormatting.highlightSearchTerms(searched, tokens, this.formattingOptions.searchPatterns); if (tokens.size > 0) { searched = TextFormatting.replaceTokens(searched, tokens); searchedContent = ( '
' + searched + '
' ); } } return ( '
' + header + '' + searchedContent + content + '' + '
' ); } codespan(text) { let output = text; if (this.formattingOptions.searchPatterns) { const tokens = new Map(); output = TextFormatting.highlightSearchTerms(output, tokens, this.formattingOptions.searchPatterns); output = TextFormatting.replaceTokens(output, tokens); } return ( '' + '' + output + '' + '' ); } br() { if (this.formattingOptions.singleline) { return ' '; } return super.br(); } image(href, title, text) { let src = href; let dimensions = []; const parts = href.split(' '); if (parts.length > 1) { const lastPart = parts.pop(); src = parts.join(' '); if (lastPart[0] === '=') { dimensions = lastPart.substr(1).split('x'); if (dimensions.length === 2 && dimensions[1] === '') { dimensions[1] = 'auto'; } } } let out = '' + text + ' 0) { out += ' width="' + dimensions[0] + '"'; } if (dimensions.length > 1) { out += ' height="' + dimensions[1] + '"'; } out += ' onload="window.markdownImageLoaded(this)" onerror="window.markdownImageLoaded(this)" class="markdown-inline-img"'; out += this.options.xhtml ? '/>' : '>'; return out; } heading(text, level) { return `${text}`; } link(href, title, text) { let outHref = href; if (this.formattingOptions.linkFilter && !this.formattingOptions.linkFilter(outHref)) { return text; } try { let unescaped = unescape(href); try { unescaped = decodeURIComponent(unescaped); } catch (e) { unescaped = global.unescape(unescaped); } unescaped = unescaped.replace(/[^\w:]/g, '').toLowerCase(); if (unescaped.indexOf('javascript:') === 0 || unescaped.indexOf('vbscript:') === 0 || unescaped.indexOf('data:') === 0) { // eslint-disable-line no-script-url return text; } } catch (e) { return text; } if (!(/[a-z+.-]+:/i).test(outHref)) { outHref = `http://${outHref}`; } let output = ']*>/g, '') + ''; return output; } paragraph(text) { if (this.formattingOptions.singleline) { return `

${text}

`; } return super.paragraph(text); } table(header, body) { return `
${header}${body}
`; } listitem(text, bullet) { const taskListReg = /^\[([ |xX])] /; const isTaskList = taskListReg.exec(text); if (isTaskList) { return `
  • ${' '}${text.replace(taskListReg, '')}
  • `; } if (/^\d+.$/.test(bullet)) { // this is a numbered list item so override the numbering return `
  • ${text}
  • `; } return `
  • ${text}
  • `; } text(txt) { return TextFormatting.doFormatText(txt, this.formattingOptions); } } export function format(text, options = {}) { const markdownOptions = { renderer: new MattermostMarkdownRenderer(null, options), sanitize: true, gfm: true, tables: true, mangle: false }; return marked(text, markdownOptions); } // Marked helper functions that should probably just be exported function unescape(html) { return html.replace(/&([#\w]+);/g, (_, m) => { const n = m.toLowerCase(); if (n === 'colon') { return ':'; } else if (n.charAt(0) === '#') { return n.charAt(1) === 'x' ? String.fromCharCode(parseInt(n.substring(2), 16)) : String.fromCharCode(Number(n.substring(1))); } return ''; }); }