diff options
-rw-r--r-- | webapp/components/code_preview.jsx | 49 | ||||
-rw-r--r-- | webapp/sass/layout/_markdown.scss | 22 | ||||
-rw-r--r-- | webapp/utils/markdown.jsx | 49 | ||||
-rw-r--r-- | webapp/utils/syntax_hightlighting.jsx | 83 |
4 files changed, 121 insertions, 82 deletions
diff --git a/webapp/components/code_preview.jsx b/webapp/components/code_preview.jsx index e769ae590..6e1af262f 100644 --- a/webapp/components/code_preview.jsx +++ b/webapp/components/code_preview.jsx @@ -2,11 +2,14 @@ // See License.txt for license information. import $ from 'jquery'; -import * as syntaxHightlighting from 'utils/syntax_hightlighting.jsx'; +import React from 'react'; + +import * as SyntaxHighlighting from 'utils/syntax_hightlighting.jsx'; import Constants from 'utils/constants.jsx'; + import FileInfoPreview from './file_info_preview.jsx'; -import React from 'react'; +import loadingGif from 'images/load.gif'; export default class CodePreview extends React.Component { constructor(props) { @@ -35,7 +38,7 @@ export default class CodePreview extends React.Component { } updateStateFromProps(props) { - var usedLanguage = syntaxHightlighting.getLang(props.filename); + var usedLanguage = SyntaxHighlighting.getLang(props.filename); if (!usedLanguage || props.fileInfo.size > Constants.CODE_PREVIEW_MAX_FILE_SIZE) { this.setState({code: '', lang: '', loading: false, success: false}); @@ -54,8 +57,7 @@ export default class CodePreview extends React.Component { } handleReceivedCode(data) { - const parsed = syntaxHightlighting.formatCode(this.state.lang, data, this.props.filename); - this.setState({code: parsed, loading: false, success: true}); + this.setState({code: data, loading: false, success: true}); } handleReceivedError() { @@ -63,7 +65,7 @@ export default class CodePreview extends React.Component { } static support(filename) { - return typeof syntaxHightlighting.getLang(filename) !== 'undefined'; + return !!SyntaxHighlighting.getLanguageFromFilename(filename); } render() { @@ -72,7 +74,7 @@ export default class CodePreview extends React.Component { <div className='view-image__loading'> <img className='loader-image' - src='/static/images/load.gif' + src={loadingGif} /> </div> ); @@ -89,7 +91,38 @@ export default class CodePreview extends React.Component { ); } - return <div dangerouslySetInnerHTML={{__html: this.state.code}}/>; + // add line numbers when viewing a code file preview + const lines = this.state.code.match(/\r\n|\r|\n|$/g).length; + let strlines = ''; + for (let i = 1; i <= lines; i++) { + if (strlines) { + strlines += '\n' + i; + } else { + strlines += i; + } + } + + const language = SyntaxHighlighting.getLanguageName(this.state.lang); + + const highlighted = SyntaxHighlighting.highlight(this.state.lang, this.state.code); + + return ( + <div className='post-code'> + <span className='post-code__language'> + {`${this.props.filename} - ${language}`} + </span> + <code className='hljs'> + <table> + <tbody> + <tr> + <td className='post-code__lineno'>{strlines}</td> + <td dangerouslySetInnerHTML={{__html: highlighted}}/> + </tr> + </tbody> + </table> + </code> + </div> + ); } } diff --git a/webapp/sass/layout/_markdown.scss b/webapp/sass/layout/_markdown.scss index 9bac332d6..04c4434ac 100644 --- a/webapp/sass/layout/_markdown.scss +++ b/webapp/sass/layout/_markdown.scss @@ -26,16 +26,13 @@ overflow-y: hidden; position: relative; - pre { - border: 1px solid rgba(221,221,221,0.2); + code { + border: 1px solid rgba(221,221,221,1); border-radius: .25em; + font-size: 13px; margin: 5px 0; - padding: 0; + padding: 6.5px; text-align: left; - } - - code { - border: none; white-space: pre; } @@ -51,6 +48,10 @@ &--wrap code { white-space: pre-wrap; } + + .hljs { + position: relative; + } } .post-code__language { @@ -78,6 +79,13 @@ user-select: none; } +.post-code__search-highlighting { + color: transparent; + pointer-events: none; + position: absolute; + @include user-select(none); +} + .post__body { hr { @include opacity(.2); diff --git a/webapp/utils/markdown.jsx b/webapp/utils/markdown.jsx index 7fd165134..6691b57b2 100644 --- a/webapp/utils/markdown.jsx +++ b/webapp/utils/markdown.jsx @@ -2,7 +2,7 @@ // See License.txt for license information. import * as TextFormatting from './text_formatting.jsx'; -import * as syntaxHightlighting from './syntax_hightlighting.jsx'; +import * as SyntaxHighlighting from './syntax_hightlighting.jsx'; import marked from 'marked'; import katex from 'katex'; @@ -43,7 +43,52 @@ class MattermostMarkdownRenderer extends marked.Renderer { usedLanguage = 'xml'; } - return syntaxHightlighting.formatCode(usedLanguage, code, null, this.formattingOptions.searchTerm); + let className = 'post-code'; + if (!usedLanguage) { + className += ' post-code--wrap'; + } + + let header = ''; + if (SyntaxHighlighting.canHighlight(usedLanguage)) { + header = ( + '<span class="post-code__language">' + + SyntaxHighlighting.getLanguageName(language) + + '</span>' + ); + } + + // 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 = ( + '<div class="post-code__search-highlighting">' + + searched + + '</div>' + ); + } + } + + return ( + '<div class="' + className + '">' + + header + + '<code class="hljs">' + + searchedContent + + content + + '</code>' + + '</div>' + ); } codespan(text) { diff --git a/webapp/utils/syntax_hightlighting.jsx b/webapp/utils/syntax_hightlighting.jsx index 33b3e2d3d..ce904c41f 100644 --- a/webapp/utils/syntax_hightlighting.jsx +++ b/webapp/utils/syntax_hightlighting.jsx @@ -123,80 +123,21 @@ hlJS.registerLanguage('yaml', hljsYaml); const HighlightedLanguages = Constants.HighlightedLanguages; -export function formatCode(lang, data, filename, searchTerm) { - const language = lang.toLowerCase() || ''; - - let contents; - let header = ''; - let className = 'post-code'; +export function highlight(lang, code) { + const language = lang.toLowerCase(); if (HighlightedLanguages[language]) { - let name = HighlightedLanguages[language].name; - - if (filename) { - const fname = decodeURIComponent(Utils.getFileName(filename)); - name = fname + ' - ' + name; - } - - header = '<span class="post-code__language">' + name + '</span>'; - try { - contents = hlJS.highlight(language, data).value; + return hlJS.highlight(language, code).value; } catch (e) { - contents = TextFormatting.sanitizeHtml(data); + // fall through if highlighting fails and handle below } - } else { - contents = TextFormatting.sanitizeHtml(data); } - if (!language) { - // wrap when no language is specified - className += ' post-code--wrap'; - - const tokens = new Map(); - contents = TextFormatting.highlightSearchTerms(contents, tokens, searchTerm); - contents = TextFormatting.replaceTokens(contents, tokens); - } - - if (filename) { - // add line numbers when viewing a code file preview - const lines = data.match(/\r\n|\r|\n|$/g).length; - let strlines = ''; - for (let i = 1; i <= lines; i++) { - if (strlines) { - strlines += '\n' + i; - } else { - strlines += i; - } - } - - contents = ( - '<table>' + - '<tbody>' + - '<tr>' + - '<td class="post-code__lineno">' + strlines + '</td>' + - '<td>' + - contents + - '</td>' + - '</tr>' + - '</tbody>' + - '</table>' - ); - } - - return ( - '<div class="' + className + '">' + - header + - '<pre>' + - '<code class="hljs">' + - contents + - '</code>' + - '</pre>' + - '</div>' - ); + return TextFormatting.sanitizeHtml(code); } -export function getLang(filename) { +export function getLanguageFromFilename(filename) { const fileInfo = Utils.splitFileLocation(filename); var ext = fileInfo.ext; if (!ext) { @@ -211,3 +152,15 @@ export function getLang(filename) { } return null; } + +export function canHighlight(language) { + return !!HighlightedLanguages[language.toLowerCase()]; +} + +export function getLanguageName(language) { + if (canHighlight(language)) { + return HighlightedLanguages[language.toLowerCase()].name; + } + + return ''; +}
\ No newline at end of file |