// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. import * as TextFormatting from './text_formatting.jsx'; import * as SyntaxHighlighting from './syntax_hightlighting.jsx'; import marked from 'marked'; import katex from 'katex'; import 'katex/dist/katex.min.css'; function markdownImageLoaded(image) { image.style.height = 'auto'; } window.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.searchTerm) { const tokens = new Map(); let searched = TextFormatting.sanitizeHtml(code); searched = TextFormatting.highlightSearchTerms(searched, tokens, this.formattingOptions.searchTerm); if (tokens.size > 0) { searched = TextFormatting.replaceTokens(searched, tokens); searchedContent = ( '
' + searched + '
' ); } } return ( '
' + header + '' + searchedContent + content + '' + '
' ); } codespan(text) { let output = text; if (this.formattingOptions.searchTerm) { const tokens = new Map(); output = TextFormatting.highlightSearchTerms(output, tokens, this.formattingOptions.searchTerm); output = TextFormatting.replaceTokens(output, tokens); } return ( '' + '' + output + '' + '' ); } br() { if (this.formattingOptions.singleline) { return ' '; } return super.br(); } image(href, title, text) { let out = '' + text + '' : '>'; return out; } heading(text, level, raw) { const id = `${this.options.headerPrefix}${raw.toLowerCase().replace(/[^\w]+/g, '-')}`; return `${text}`; } link(href, title, text) { let outHref = href; try { const unescaped = decodeURIComponent(unescape(href)).replace(/[^\w:]/g, '').toLowerCase(); if (unescaped.indexOf('javascript:') === 0 || unescaped.indexOf('vbscript:') === 0 || unescaped.indexOf('data:') === 0) { // eslint-disable-line no-script-url return ''; } } catch (e) { return ''; } if (!(/[a-z+.-]+:/i).test(outHref)) { outHref = `http://${outHref}`; } let output = ']*>/, '') + ''; 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 }; 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(+n.substring(1)); } return ''; }); }