From c424c585a54b6d2f4e71087dd0cfc3d7a05f7c14 Mon Sep 17 00:00:00 2001 From: Daniel Espino Date: Fri, 10 Jan 2020 06:11:05 -0500 Subject: [PATCH 1/6] Add markdown support to Todos --- webapp/package-lock.json | 98 ++++++ webapp/package.json | 6 +- .../components/sidebar_right/todo_items.jsx | 9 +- webapp/src/utils/markdown/index.js | 35 +++ webapp/src/utils/markdown/index.test.js | 33 ++ .../src/utils/markdown/link_only_renderer.jsx | 31 ++ .../markdown/link_only_renderer.test.jsx | 293 +++++++++++++++++ .../utils/markdown/mentionable_renderer.jsx | 81 +++++ webapp/src/utils/markdown/remove_markdown.js | 78 +++++ .../utils/markdown/remove_markdown.test.js | 297 ++++++++++++++++++ webapp/src/utils/markdown/renderer.jsx | 205 ++++++++++++ .../src/utils/message_html_to_component.jsx | 45 +++ webapp/src/utils/text_formatting.jsx | 268 ++++++++++++++++ webapp/src/utils/url.jsx | 24 ++ 14 files changed, 1499 insertions(+), 4 deletions(-) create mode 100644 webapp/src/utils/markdown/index.js create mode 100644 webapp/src/utils/markdown/index.test.js create mode 100644 webapp/src/utils/markdown/link_only_renderer.jsx create mode 100644 webapp/src/utils/markdown/link_only_renderer.test.jsx create mode 100644 webapp/src/utils/markdown/mentionable_renderer.jsx create mode 100644 webapp/src/utils/markdown/remove_markdown.js create mode 100644 webapp/src/utils/markdown/remove_markdown.test.js create mode 100644 webapp/src/utils/markdown/renderer.jsx create mode 100644 webapp/src/utils/message_html_to_component.jsx create mode 100644 webapp/src/utils/text_formatting.jsx create mode 100644 webapp/src/utils/url.jsx diff --git a/webapp/package-lock.json b/webapp/package-lock.json index 9d013000..c4c8c53d 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -3217,6 +3217,22 @@ } } }, + "@babel/runtime-corejs2": { + "version": "7.7.7", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs2/-/runtime-corejs2-7.7.7.tgz", + "integrity": "sha512-P91T3dFYQL7aj44PxOMIAbo66Ag3NbmXG9fseSYaXxapp3K9XTct5HU9IpTOm2D0AoktKusgqzN5YcSxZXEKBQ==", + "requires": { + "core-js": "^2.6.5", + "regenerator-runtime": "^0.13.2" + }, + "dependencies": { + "regenerator-runtime": { + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz", + "integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==" + } + } + }, "@babel/template": { "version": "7.4.4", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.4.4.tgz", @@ -5261,12 +5277,26 @@ "@babel/runtime": "^7.1.2" } }, + "dom-serializer": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", + "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", + "requires": { + "domelementtype": "^2.0.1", + "entities": "^2.0.0" + } + }, "domain-browser": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", "dev": true }, + "domelementtype": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz", + "integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==" + }, "domexception": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", @@ -5276,6 +5306,24 @@ "webidl-conversions": "^4.0.2" } }, + "domhandler": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.0.0.tgz", + "integrity": "sha512-eKLdI5v9m67kbXQbJSNn1zjh0SDzvzWVWtX+qEI3eMjZw8daH9k8rlj1FZY9memPwjiskQFbe7vHVVJIAqoEhw==", + "requires": { + "domelementtype": "^2.0.1" + } + }, + "domutils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.0.0.tgz", + "integrity": "sha512-n5SelJ1axbO636c2yUtOGia/IcJtVtlhQbFiVDBZHKV5ReJO1ViX7sFEemtuyoAnBxk5meNSYgA8V4s0271efg==", + "requires": { + "dom-serializer": "^0.2.1", + "domelementtype": "^2.0.1", + "domhandler": "^3.0.0" + } + }, "duplexify": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", @@ -5353,6 +5401,11 @@ "tapable": "^0.1.8" } }, + "entities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz", + "integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==" + }, "errno": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", @@ -7211,6 +7264,28 @@ "whatwg-encoding": "^1.0.1" } }, + "html-to-react": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/html-to-react/-/html-to-react-1.4.2.tgz", + "integrity": "sha512-TdTfxd95sRCo6QL8admCkE7mvNNrXtGoVr1dyS+7uvc8XCqAymnf/6ckclvnVbQNUo2Nh21VPwtfEHd0khiV7g==", + "requires": { + "domhandler": "^3.0", + "htmlparser2": "^4.0", + "lodash.camelcase": "^4.3.0", + "ramda": "^0.26" + } + }, + "htmlparser2": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-4.0.0.tgz", + "integrity": "sha512-cChwXn5Vam57fyXajDtPXL1wTYc8JtLbr2TN76FYu05itVVVealxLowe2B3IEznJG4p9HAYn/0tJaRlGuEglFQ==", + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^3.0.0", + "domutils": "^2.0.0", + "entities": "^2.0.0" + } + }, "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -8673,6 +8748,11 @@ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.14.tgz", "integrity": "sha512-7zchRrGa8UZXjD/4ivUWP1867jDkhzTG2c/uj739utSd7O/pFFdxspCemIFKEEjErbcqRzn8nKnGsi7mvTgRPA==" }, + "lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" + }, "lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -8781,6 +8861,11 @@ "object-visit": "^1.0.0" } }, + "marked": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-0.8.0.tgz", + "integrity": "sha512-MyUe+T/Pw4TZufHkzAfDj6HarCBWia2y27/bhuYkTaiUnfDYFnCP3KUN+9oM7Wi6JA2rymtVYbQu3spE0GCmxQ==" + }, "mattermost-redux": { "version": "5.14.0", "resolved": "https://registry.npmjs.org/mattermost-redux/-/mattermost-redux-5.14.0.tgz", @@ -10196,6 +10281,11 @@ "performance-now": "^2.1.0" } }, + "ramda": { + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.26.1.tgz", + "integrity": "sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ==" + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -12613,6 +12703,14 @@ "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", "dev": true }, + "xregexp": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.2.4.tgz", + "integrity": "sha512-sO0bYdYeJAJBcJA8g7MJJX7UrOZIfJPd8U2SC7B2Dd/J24U0aQNoGp33shCaBSWeb0rD5rh6VBUIXOkGal1TZA==", + "requires": { + "@babel/runtime-corejs2": "^7.2.0" + } + }, "xtend": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", diff --git a/webapp/package.json b/webapp/package.json index fc696145..98c0292a 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -37,11 +37,15 @@ "webpack-cli": "3.3.5" }, "dependencies": { + "html-to-react": "^1.4.2", + "marked": "^0.8.0", "mattermost-redux": "5.14.0", + "mattermost-webapp": "5.14.0", "react": "16.8.6", "react-custom-scrollbars": "^4.2.1", "react-redux": "5.0.7", "react-transition-group": "4.2.2", - "redux": "4.0.1" + "redux": "4.0.1", + "xregexp": "^4.2.4" } } diff --git a/webapp/src/components/sidebar_right/todo_items.jsx b/webapp/src/components/sidebar_right/todo_items.jsx index 370ba616..24362648 100644 --- a/webapp/src/components/sidebar_right/todo_items.jsx +++ b/webapp/src/components/sidebar_right/todo_items.jsx @@ -5,6 +5,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import {makeStyleFromTheme, changeOpacity} from 'mattermost-redux/utils/theme_utils'; +import messageHtmlToComponent from 'utils/message_html_to_component'; +import * as TextFormatting from 'utils/text_formatting.jsx'; const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; @@ -22,6 +24,9 @@ function ToDoItems(props) { const formattedTime = hours + ':' + minutes.substr(-2) + ':' + seconds.substr(-2); const formattedDate = month + ' ' + day + ', ' + year; + const htmlFormattedText = TextFormatting.formatText(item.message); + const itemComponent = messageHtmlToComponent(htmlFormattedText, true); + return (
- - {item.message} - + {itemComponent}
0) { + return convertEntityToCharacter(formatWithRenderer(text, removeMarkdown)).trim(); + } + + return text; +} diff --git a/webapp/src/utils/markdown/index.test.js b/webapp/src/utils/markdown/index.test.js new file mode 100644 index 00000000..17b8afb6 --- /dev/null +++ b/webapp/src/utils/markdown/index.test.js @@ -0,0 +1,33 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {format} from './index'; + +describe('format', () => { + test('should highlight code without space before language', () => { + const output = format(`~~~diff +- something ++ something else +~~~`); + + expect(output).toContain('Diff'); + expect(output).toContain(''); + }); + + test('should highlight code with space before language', () => { + const output = format(`~~~ diff +- something ++ something else +~~~`); + + expect(output).toContain('Diff'); + expect(output).toContain(''); + }); + + test('should not highlight code with an invalid language', () => { + const output = format(`~~~garbage +~~~`); + + expect(output).not.toContain(''); + }); +}); diff --git a/webapp/src/utils/markdown/link_only_renderer.jsx b/webapp/src/utils/markdown/link_only_renderer.jsx new file mode 100644 index 00000000..67eef97b --- /dev/null +++ b/webapp/src/utils/markdown/link_only_renderer.jsx @@ -0,0 +1,31 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import RemoveMarkdown from './remove_markdown'; +import {getScheme} from 'utils/url'; + +export default class LinkOnlyRenderer extends RemoveMarkdown { + link(href, title, text) { + let outHref = href; + + if (!getScheme(href)) { + outHref = `http://${outHref}`; + } + + let output = `${text}`; + + return output; + } +} + +function getScheme(url) { + const match = (/([a-z0-9+.-]+):/i).exec(url); + + return match && match[1]; +} diff --git a/webapp/src/utils/markdown/link_only_renderer.test.jsx b/webapp/src/utils/markdown/link_only_renderer.test.jsx new file mode 100644 index 00000000..5ec3db11 --- /dev/null +++ b/webapp/src/utils/markdown/link_only_renderer.test.jsx @@ -0,0 +1,293 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import LinkOnlyRenderer from 'utils/markdown/link_only_renderer'; +import {formatWithRenderer} from 'utils/markdown'; + +describe('formatWithRenderer | LinkOnlyRenderer', () => { + const testCases = [ + { + description: 'emoji: same', + inputText: 'Hey :smile: :+1: :)', + outputText: 'Hey :smile: :+1: :)', + }, + { + description: 'at-mention: same', + inputText: 'Hey @user and @test', + outputText: 'Hey @user and @test', + }, + { + description: 'channel-link: same', + inputText: 'join ~channelname', + outputText: 'join ~channelname', + }, + { + description: 'codespan: single backtick', + inputText: '`single backtick`', + outputText: 'single backtick', + }, + { + description: 'codespan: double backtick', + inputText: '``double backtick``', + outputText: 'double backtick', + }, + { + description: 'codespan: triple backtick', + inputText: '```triple backtick```', + outputText: 'triple backtick', + }, + { + description: 'codespan: inline code', + inputText: 'Inline `code` has ``double backtick`` and ```triple backtick``` around it.', + outputText: 'Inline code has double backtick and triple backtick around it.', + }, + { + description: 'code block: single line code block', + inputText: 'Code block\n```\nline\n```', + outputText: 'Code block line', + }, + { + description: 'code block: multiline code block 2', + inputText: 'Multiline\n```function(number) {\n return number + 1;\n}```', + outputText: 'Multiline function(number) { return number + 1; }', + }, + { + description: 'code block: language highlighting', + inputText: '```javascript\nvar s = "JavaScript syntax highlighting";\nalert(s);\n```', + outputText: 'var s = "JavaScript syntax highlighting"; alert(s);', + }, + { + description: 'blockquote:', + inputText: '> Hey quote', + outputText: 'Hey quote', + }, + { + description: 'blockquote: multiline', + inputText: '> Hey quote.\n> Hello quote.', + outputText: 'Hey quote. Hello quote.', + }, + { + description: 'heading: # H1 header', + inputText: '# H1 header', + outputText: 'H1 header', + }, + { + description: 'heading: heading with @user', + inputText: '# H1 @user', + outputText: 'H1 @user', + }, + { + description: 'heading: ## H2 header', + inputText: '## H2 header', + outputText: 'H2 header', + }, + { + description: 'heading: ### H3 header', + inputText: '### H3 header', + outputText: 'H3 header', + }, + { + description: 'heading: #### H4 header', + inputText: '#### H4 header', + outputText: 'H4 header', + }, + { + description: 'heading: ##### H5 header', + inputText: '##### H5 header', + outputText: 'H5 header', + }, + { + description: 'heading: ###### H6 header', + inputText: '###### H6 header', + outputText: 'H6 header', + }, + { + description: 'heading: multiline with header and paragraph', + inputText: '###### H6 header\nThis is next line.\nAnother line.', + outputText: 'H6 header This is next line. Another line.', + }, + { + description: 'heading: multiline with header and list items', + inputText: '###### H6 header\n- list item 1\n- list item 2', + outputText: 'H6 header list item 1 list item 2', + }, + { + description: 'heading: multiline with header and links', + inputText: '###### H6 header\n[link 1](https://mattermost.com) - [link 2](https://mattermost.com)', + outputText: 'H6 header ' + + 'link 1 - link 2', + }, + { + description: 'list: 1. First ordered list item', + inputText: '1. First ordered list item', + outputText: 'First ordered list item', + }, + { + description: 'list: 2. Another item', + inputText: '1. 2. Another item', + outputText: 'Another item', + }, + { + description: 'list: * Unordered sub-list.', + inputText: '* Unordered sub-list.', + outputText: 'Unordered sub-list.', + }, + { + description: 'list: - Or minuses', + inputText: '- Or minuses', + outputText: 'Or minuses', + }, + { + description: 'list: + Or pluses', + inputText: '+ Or pluses', + outputText: 'Or pluses', + }, + { + description: 'list: multiline', + inputText: '1. First ordered list item\n2. Another item', + outputText: 'First ordered list item Another item', + }, + { + description: 'tablerow:)', + inputText: 'Markdown | Less | Pretty\n' + + '--- | --- | ---\n' + + '*Still* | `renders` | **nicely**\n' + + '1 | 2 | 3\n', + outputText: '', + }, + { + description: 'table:', + inputText: '| Tables | Are | Cool |\n' + + '| ------------- |:-------------:| -----:|\n' + + '| col 3 is | right-aligned | $1600 |\n' + + '| col 2 is | centered | $12 |\n' + + '| zebra stripes | are neat | $1 |\n', + outputText: '', + }, + { + description: 'strong: Bold with **asterisks** or __underscores__.', + inputText: 'Bold with **asterisks** or __underscores__.', + outputText: 'Bold with asterisks or underscores.', + }, + { + description: 'strong & em: Bold and italics with **asterisks and _underscores_**.', + inputText: 'Bold and italics with **asterisks and _underscores_**.', + outputText: 'Bold and italics with asterisks and underscores.', + }, + { + description: 'em: Italics with *asterisks* or _underscores_.', + inputText: 'Italics with *asterisks* or _underscores_.', + outputText: 'Italics with asterisks or underscores.', + }, + { + description: 'del: Strikethrough ~~strike this.~~', + inputText: 'Strikethrough ~~strike this.~~', + outputText: 'Strikethrough strike this.', + }, + { + description: 'links: [inline-style link](http://localhost:8065)', + inputText: '[inline-style link](http://localhost:8065)', + outputText: '' + + 'inline-style link', + }, + { + description: 'image: ![image link](http://localhost:8065/image)', + inputText: '![image link](http://localhost:8065/image)', + outputText: 'image link', + }, + { + description: 'text: plain', + inputText: 'This is plain text.', + outputText: 'This is plain text.', + }, + { + description: 'text: multiline', + inputText: 'This is multiline text.\nHere is the next line.\n', + outputText: 'This is multiline text. Here is the next line.', + }, + { + description: 'text: & entity', + inputText: 'you & me', + outputText: 'you & me', + }, + { + description: 'text: < entity', + inputText: '1<2', + outputText: '1<2', + }, + { + description: 'text: > entity', + inputText: '2>1', + outputText: '2>1', + }, + { + description: 'text: ' entity', + inputText: 'he\'s out', + outputText: 'he's out', + }, + { + description: 'text: " entity', + inputText: 'That is "unique"', + outputText: 'That is "unique"', + }, + { + description: 'text: multiple entities', + inputText: '&<>\'"', + outputText: '&<>'"', + }, + { + description: 'text: multiple entities', + inputText: '"\'><&', + outputText: '"'><&', + }, + { + description: 'text: multiple entities', + inputText: '&<>'"', + outputText: '&<>'"', + }, + { + description: 'text: multiple entities', + inputText: '"'><&', + outputText: '"'><&', + }, + { + description: 'text: multiple entities', + inputText: '&lt;', + outputText: '&lt;', + }, + { + description: 'text: empty string', + inputText: '', + outputText: '', + }, + { + description: 'link: link without a scheme', + inputText: 'Do you like www.mattermost.com?', + outputText: 'Do you like ' + + 'www.mattermost.com?', + }, + { + description: 'link: link with a scheme', + inputText: 'Do you like http://www.mattermost.com?', + outputText: 'Do you like ' + + 'http://www.mattermost.com?', + }, + { + description: 'link: link with a title', + inputText: 'Do you like [Mattermost](http://www.mattermost.com)?', + outputText: 'Do you like ' + + 'Mattermost?', + }, + { + description: 'link: link with curly brackets', + inputText: 'Let\'s try http://example/result?things={stuff}', + outputText: 'Let's try http://example/result?things={stuff}', + }, + ]; + + const linkOnlyRenderer = new LinkOnlyRenderer(); + + testCases.forEach((testCase) => it(testCase.description, () => { + expect(formatWithRenderer(testCase.inputText, linkOnlyRenderer)).toEqual(testCase.outputText); + })); +}); diff --git a/webapp/src/utils/markdown/mentionable_renderer.jsx b/webapp/src/utils/markdown/mentionable_renderer.jsx new file mode 100644 index 00000000..44d1bad1 --- /dev/null +++ b/webapp/src/utils/markdown/mentionable_renderer.jsx @@ -0,0 +1,81 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import marked from 'marked'; + +/** A Markdown renderer that converts a post into plain text that we can search for mentions */ +export default class MentionableRenderer extends marked.Renderer { + code() { + // Code blocks can't contain mentions + return '\n'; + } + + blockquote(text) { + return text + '\n'; + } + + heading(text) { + return text + '\n'; + } + + hr() { + return '\n'; + } + + list(body) { + return body + '\n'; + } + + listitem(text) { + return text + '\n'; + } + + paragraph(text) { + return text + '\n'; + } + + table(header, body) { + return header + '\n' + body; + } + + tablerow(content) { + return content; + } + + tablecell(content) { + return content + '\n'; + } + + strong(text) { + return ' ' + text + ' '; + } + + em(text) { + return ' ' + text + ' '; + } + + codespan() { + // Code spans can't contain mentions + return ' '; + } + + br() { + return '\n'; + } + + del(text) { + return ' ' + text + ' '; + } + + link(href, title, text) { + return ' ' + text + ' '; + } + + image(href, title, text) { + return ' ' + text + ' '; + } + + text(text) { + return text; + } +} diff --git a/webapp/src/utils/markdown/remove_markdown.js b/webapp/src/utils/markdown/remove_markdown.js new file mode 100644 index 00000000..e2e5bd81 --- /dev/null +++ b/webapp/src/utils/markdown/remove_markdown.js @@ -0,0 +1,78 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import marked from 'marked'; + +export default class RemoveMarkdown extends marked.Renderer { + code(text) { + return text.replace(/\n/g, ' '); + } + + blockquote(text) { + return text.replace(/\n/g, ' '); + } + + heading(text) { + return text + ' '; + } + + hr() { + return ''; + } + + list(body) { + return body; + } + + listitem(text) { + return text + ' '; + } + + paragraph(text) { + return text + ' '; + } + + table() { + return ''; + } + + tablerow() { + return ''; + } + + tablecell() { + return ''; + } + + strong(text) { + return text; + } + + em(text) { + return text; + } + + codespan(text) { + return text.replace(/\n/g, ' '); + } + + br() { + return ' '; + } + + del(text) { + return text; + } + + link(href, title, text) { + return text; + } + + image(href, title, text) { + return text; + } + + text(text) { + return text.replace('\n', ' '); + } +} diff --git a/webapp/src/utils/markdown/remove_markdown.test.js b/webapp/src/utils/markdown/remove_markdown.test.js new file mode 100644 index 00000000..aa2bcb3e --- /dev/null +++ b/webapp/src/utils/markdown/remove_markdown.test.js @@ -0,0 +1,297 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; + +import {stripMarkdown} from 'utils/markdown'; + +describe('stripMarkdown | RemoveMarkdown', () => { + const testCases = [ + { + description: 'emoji: same', + inputText: 'Hey :smile: :+1: :)', + outputText: 'Hey :smile: :+1: :)', + }, + { + description: 'at-mention: same', + inputText: 'Hey @user and @test', + outputText: 'Hey @user and @test', + }, + { + description: 'channel-link: same', + inputText: 'join ~channelname', + outputText: 'join ~channelname', + }, + { + description: 'codespan: single backtick', + inputText: '`single backtick`', + outputText: 'single backtick', + }, + { + description: 'codespan: double backtick', + inputText: '``double backtick``', + outputText: 'double backtick', + }, + { + description: 'codespan: triple backtick', + inputText: '```triple backtick```', + outputText: 'triple backtick', + }, + { + description: 'codespan: inline code', + inputText: 'Inline `code` has ``double backtick`` and ```triple backtick``` around it.', + outputText: 'Inline code has double backtick and triple backtick around it.', + }, + { + description: 'code block: single line code block', + inputText: 'Code block\n```\nline\n```', + outputText: 'Code block line', + }, + { + description: 'code block: multiline code block 2', + inputText: 'Multiline\n```function(number) {\n return number + 1;\n}```', + outputText: 'Multiline function(number) { return number + 1; }', + }, + { + description: 'code block: language highlighting', + inputText: '```javascript\nvar s = "JavaScript syntax highlighting";\nalert(s);\n```', + outputText: 'var s = "JavaScript syntax highlighting"; alert(s);', + }, + { + description: 'blockquote:', + inputText: '> Hey quote', + outputText: 'Hey quote', + }, + { + description: 'blockquote: multiline', + inputText: '> Hey quote.\n> Hello quote.', + outputText: 'Hey quote. Hello quote.', + }, + { + description: 'heading: # H1 header', + inputText: '# H1 header', + outputText: 'H1 header', + }, + { + description: 'heading: heading with @user', + inputText: '# H1 @user', + outputText: 'H1 @user', + }, + { + description: 'heading: ## H2 header', + inputText: '## H2 header', + outputText: 'H2 header', + }, + { + description: 'heading: ### H3 header', + inputText: '### H3 header', + outputText: 'H3 header', + }, + { + description: 'heading: #### H4 header', + inputText: '#### H4 header', + outputText: 'H4 header', + }, + { + description: 'heading: ##### H5 header', + inputText: '##### H5 header', + outputText: 'H5 header', + }, + { + description: 'heading: ###### H6 header', + inputText: '###### H6 header', + outputText: 'H6 header', + }, + { + description: 'heading: multiline with header and paragraph', + inputText: '###### H6 header\nThis is next line.\nAnother line.', + outputText: 'H6 header This is next line. Another line.', + }, + { + description: 'heading: multiline with header and list items', + inputText: '###### H6 header\n- list item 1\n- list item 2', + outputText: 'H6 header list item 1 list item 2', + }, + { + description: 'heading: multiline with header and links', + inputText: '###### H6 header\n[link 1](https://mattermost.com) - [link 2](https://mattermost.com)', + outputText: 'H6 header link 1 - link 2', + }, + { + description: 'list: 1. First ordered list item', + inputText: '1. First ordered list item', + outputText: 'First ordered list item', + }, + { + description: 'list: 2. Another item', + inputText: '1. 2. Another item', + outputText: 'Another item', + }, + { + description: 'list: * Unordered sub-list.', + inputText: '* Unordered sub-list.', + outputText: 'Unordered sub-list.', + }, + { + description: 'list: - Or minuses', + inputText: '- Or minuses', + outputText: 'Or minuses', + }, + { + description: 'list: + Or pluses', + inputText: '+ Or pluses', + outputText: 'Or pluses', + }, + { + description: 'list: multiline', + inputText: '1. First ordered list item\n2. Another item', + outputText: 'First ordered list item Another item', + }, + { + description: 'tablerow:)', + inputText: 'Markdown | Less | Pretty\n' + + '--- | --- | ---\n' + + '*Still* | `renders` | **nicely**\n' + + '1 | 2 | 3\n', + outputText: '', + }, + { + description: 'table:', + inputText: '| Tables | Are | Cool |\n' + + '| ------------- |:-------------:| -----:|\n' + + '| col 3 is | right-aligned | $1600 |\n' + + '| col 2 is | centered | $12 |\n' + + '| zebra stripes | are neat | $1 |\n', + outputText: '', + }, + { + description: 'strong: Bold with **asterisks** or __underscores__.', + inputText: 'Bold with **asterisks** or __underscores__.', + outputText: 'Bold with asterisks or underscores.', + }, + { + description: 'strong & em: Bold and italics with **asterisks and _underscores_**.', + inputText: 'Bold and italics with **asterisks and _underscores_**.', + outputText: 'Bold and italics with asterisks and underscores.', + }, + { + description: 'em: Italics with *asterisks* or _underscores_.', + inputText: 'Italics with *asterisks* or _underscores_.', + outputText: 'Italics with asterisks or underscores.', + }, + { + description: 'del: Strikethrough ~~strike this.~~', + inputText: 'Strikethrough ~~strike this.~~', + outputText: 'Strikethrough strike this.', + }, + { + description: 'links: [inline-style link](http://localhost:8065)', + inputText: '[inline-style link](http://localhost:8065)', + outputText: 'inline-style link', + }, + { + description: 'image: ![image link](http://localhost:8065/image)', + inputText: '![image link](http://localhost:8065/image)', + outputText: 'image link', + }, + { + description: 'text: plain', + inputText: 'This is plain text.', + outputText: 'This is plain text.', + }, + { + description: 'text: multiline', + inputText: 'This is multiline text.\nHere is the next line.\n', + outputText: 'This is multiline text. Here is the next line.', + }, + { + description: 'text: multiline with blockquote', + inputText: 'This is multiline text.\n> With quote', + outputText: 'This is multiline text. With quote', + }, + { + description: 'text: multiline with list items', + inputText: 'This is multiline text.\n * List item ', + outputText: 'This is multiline text. List item', + }, + { + description: 'text: & entity', + inputText: 'you & me', + outputText: 'you & me', + }, + { + description: 'text: < entity', + inputText: '1<2', + outputText: '1<2', + }, + { + description: 'text: > entity', + inputText: '2>1', + outputText: '2>1', + }, + { + description: 'text: ' entity', + inputText: 'he\'s out', + outputText: 'he\'s out', + }, + { + description: 'text: " entity', + inputText: 'That is "unique"', + outputText: 'That is "unique"', + }, + { + description: 'text: multiple entities', + inputText: '&<>\'"', + outputText: '&<>\'"', + }, + { + description: 'text: multiple entities', + inputText: '"\'><&', + outputText: '"\'><&', + }, + { + description: 'text: multiple entities', + inputText: '&<>'"', + outputText: '&<>\'"', + }, + { + description: 'text: multiple entities', + inputText: '"'><&', + outputText: '"\'><&', + }, + { + description: 'text: multiple entities', + inputText: '&lt;', + outputText: '<', + }, + { + description: 'text: empty string', + inputText: '', + outputText: '', + }, + { + description: 'text: null', + inputText: null, + outputText: null, + }, + { + description: 'text: {}', + inputText: {key: 'value'}, + outputText: {key: 'value'}, + }, + { + description: 'text: []', + inputText: [1], + outputText: [1], + }, + { + description: 'text: node', + inputText: (
), + outputText: (
), + }, + ]; + + testCases.forEach((testCase) => it(testCase.description, () => { + expect(stripMarkdown(testCase.inputText)).toEqual(testCase.outputText); + })); +}); diff --git a/webapp/src/utils/markdown/renderer.jsx b/webapp/src/utils/markdown/renderer.jsx new file mode 100644 index 00000000..e29f18dc --- /dev/null +++ b/webapp/src/utils/markdown/renderer.jsx @@ -0,0 +1,205 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import marked from 'marked'; + +import * as TextFormatting from 'utils/text_formatting.jsx'; +import {getScheme, isUrlSafe} from 'utils/url.jsx'; + +export default class Renderer 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') { + return `
`; + } + + // treat html as xml to prevent injection attacks + if (usedLanguage === 'html') { + usedLanguage = 'xml'; + } + + let className = 'post-code'; + let codeClassName = 'hljs hljs-ln'; + className += ' post-code--wrap'; + codeClassName = 'hljs'; + + let header = ''; + + // 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 = code; + let searchedContent = ''; + + return ( + '
' + + header + + '' + + searchedContent + + content + + '' + + '
' + ); + } + + codespan(text) { + let output = text; + + if (this.formattingOptions.searchPatterns) { + const tokens = new Map(); + output = TextFormatting.replaceTokens(output, tokens); + } + + return ( + '' + + '' + + output + + '' + + '' + ); + } + + br() { + if (this.formattingOptions.singleline) { + return ' '; + } + + return super.br(); + } + + heading(text, level) { + return `${text}`; + } + + link(href, title, text, isUrl) { + let outHref = href; + + if (!href.startsWith('/')) { + const scheme = getScheme(href); + if (!scheme) { + outHref = `http://${outHref}`; + } else if (isUrl && this.formattingOptions.autolinkedUrlSchemes) { + const isValidUrl = this.formattingOptions.autolinkedUrlSchemes.indexOf(scheme.toLowerCase()) !== -1; + + if (!isValidUrl) { + return text; + } + } + } + + if (!isUrlSafe(unescapeHtmlEntities(href))) { + return text; + } + + let output = ']*>/g, '') + ''; + + return output; + } + + paragraph(text) { + if (this.formattingOptions.singleline) { + let result; + if (text.includes('class="markdown-inline-img"')) { + /* + ** use a div tag instead of a p tag to allow other divs to be nested, + ** which avoids errors of incorrect DOM nesting (
inside

) + */ + result = `

${text}
`; + } else { + result = `

${text}

`; + } + return result; + } + + return super.paragraph(text); + } + + table(header, body) { + return `
${header}${body}
`; + } + + tablerow(content) { + return `${content}`; + } + + tablecell(content, flags) { + return marked.Renderer.prototype.tablecell(content, flags).trim(); + } + + 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); + } +} + +// Marked helper functions that should probably just be exported + +function unescapeHtmlEntities(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 ''; + }); +} diff --git a/webapp/src/utils/message_html_to_component.jsx b/webapp/src/utils/message_html_to_component.jsx new file mode 100644 index 00000000..7eb1c48e --- /dev/null +++ b/webapp/src/utils/message_html_to_component.jsx @@ -0,0 +1,45 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {Parser, ProcessNodeDefinitions} from 'html-to-react'; + +/* + * Converts HTML to React components using html-to-react. + */ +export function messageHtmlToComponent(html, isRHS, options = {}) { + if (!html) { + return null; + } + + const parser = new Parser(); + const processNodeDefinitions = new ProcessNodeDefinitions(React); + + function isValidNode() { + return true; + } + + const processingInstructions = [ + + // Workaround to fix MM-14931 + { + replaceChildren: false, + shouldProcessNode: (node) => node.type === 'tag' && node.name === 'input' && node.attribs.type === 'checkbox', + processNode: (node) => { + const attribs = node.attribs || {}; + node.attribs.checked = Boolean(attribs.checked); + + return React.createElement('input', {...node.attribs}); + }, + }, + ]; + + processingInstructions.push({ + shouldProcessNode: () => true, + processNode: processNodeDefinitions.processDefaultNode, + }); + + return parser.parseWithInstructions(html, isValidNode, processingInstructions); +} + +export default messageHtmlToComponent; diff --git a/webapp/src/utils/text_formatting.jsx b/webapp/src/utils/text_formatting.jsx new file mode 100644 index 00000000..52cbcd74 --- /dev/null +++ b/webapp/src/utils/text_formatting.jsx @@ -0,0 +1,268 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import XRegExp from 'xregexp'; + +import * as Markdown from './markdown'; + +const punctuation = XRegExp.cache('[^\\pL\\d]'); + +const htmlEmojiPattern = /^

    \s*(?:]*>|]*>[^<]*<\/span>\s*|[^<]*<\/span>\s*)+<\/p>$/; + +// Performs formatting of user posts including converting urls, hashtags, +// @mentions and ~channels to links by taking a user's message and returning a string of formatted html. Also takes +// a number of options as part of the second parameter: +// - singleline - Specifies whether or not to remove newlines. Defaults to false. +// - markdown - Enables markdown parsing. Defaults to true. +// - siteURL - The origin of this Mattermost instance. If provided, links to channels and posts will be replaced with internal +// links that can be handled by a special click handler. +// - channelNamesMap - An object mapping channel display names to channels. If provided, ~channel mentions will be replaced with +// links to the relevant channel. +// - team - The current team. +// - minimumHashtagLength - Minimum number of characters in a hashtag. Defaults to 3. +export function formatText(text, inputOptions) { + if (!text || typeof text !== 'string') { + return ''; + } + + let output = text; + const options = Object.assign({}, inputOptions); + + if (!('markdown' in options) || options.markdown) { + // the markdown renderer will call doFormatText as necessary + output = Markdown.format(output, options); + if (output.includes('class="markdown-inline-img"')) { + /* + ** remove p tag to allow other divs to be nested, + ** which allows markdown images to open preview window + */ + const replacer = (match) => { + return match === '

    ' ? '

    ' : '
    '; + }; + output = output.replace(/

    |<\/p>/g, replacer); + } + } else { + output = sanitizeHtml(output); + output = doFormatText(output, options); + } + + // replace newlines with spaces if necessary + if (options.singleline) { + output = replaceNewlines(output); + } + + if (htmlEmojiPattern.test(output.trim())) { + output = '' + output.trim() + ''; + } + + return output; +} + +// Performs most of the actual formatting work for formatText. Not intended to be called normally. +export function doFormatText(text, options) { + let output = text; + + const tokens = new Map(); + + if (options.channelNamesMap) { + output = autolinkChannelMentions(output, tokens, options.channelNamesMap, options.team); + } + + output = autolinkEmails(output, tokens); + output = autolinkHashtags(output, tokens, options.minimumHashtagLength); + + // reinsert tokens with formatted versions of the important words and phrases + output = replaceTokens(output, tokens); + + return output; +} + +export function sanitizeHtml(text) { + let output = text; + + // normal string.replace only does a single occurrance so use a regex instead + output = output.replace(/&/g, '&'); + output = output.replace(//g, '>'); + output = output.replace(/'/g, '''); + output = output.replace(/"/g, '"'); + + return output; +} + +// Copied from our fork of commonmark.js +var emailAlphaNumericChars = '\\p{L}\\p{Nd}'; +var emailSpecialCharacters = '!#$%&\'*+\\-\\/=?^_`{|}~'; +var emailRestrictedSpecialCharacters = '\\s(),:;<>@\\[\\]'; +var emailValidCharacters = emailAlphaNumericChars + emailSpecialCharacters; +var emailValidRestrictedCharacters = emailValidCharacters + emailRestrictedSpecialCharacters; +var emailStartPattern = '(?:[' + emailValidCharacters + '](?:[' + emailValidCharacters + ']|\\.(?!\\.|@))*|\\"[' + emailValidRestrictedCharacters + '.]+\\")@'; +var reEmail = XRegExp.cache('(^|[^\\pL\\d])(' + emailStartPattern + '[\\pL\\d.\\-]+[.]\\pL{2,4}(?=$|[^\\p{L}]))', 'g'); + +// Convert emails into tokens +function autolinkEmails(text, tokens) { + function replaceEmailWithToken(fullMatch, prefix, email) { + const index = tokens.size; + const alias = `$MM_EMAIL${index}$`; + + tokens.set(alias, { + value: `${email}`, + originalText: email, + }); + + return prefix + alias; + } + + let output = text; + output = XRegExp.replace(text, reEmail, replaceEmailWithToken); + + return output; +} + +function autolinkChannelMentions(text, tokens, channelNamesMap, team) { + function channelMentionExists(c) { + return Boolean(channelNamesMap[c]); + } + function addToken(channelName, mention, displayName) { + const index = tokens.size; + const alias = `$MM_CHANNELMENTION${index}$`; + let href = '#'; + if (team) { + href = (window.basename || '') + '/' + team.name + '/channels/' + channelName; + } + + tokens.set(alias, { + value: `~${displayName}`, + originalText: mention, + }); + return alias; + } + + function replaceChannelMentionWithToken(fullMatch, mention, channelName) { + let channelNameLower = channelName.toLowerCase(); + + if (channelMentionExists(channelNameLower)) { + // Exact match + const alias = addToken(channelNameLower, mention, escapeHtml(channelNamesMap[channelNameLower].display_name)); + return alias; + } + + // Not an exact match, attempt to truncate any punctuation to see if we can find a channel + const originalChannelName = channelNameLower; + + for (let c = channelNameLower.length; c > 0; c--) { + if (punctuation.test(channelNameLower[c - 1])) { + channelNameLower = channelNameLower.substring(0, c - 1); + + if (channelMentionExists(channelNameLower)) { + const suffix = originalChannelName.substr(c - 1); + const alias = addToken(channelNameLower, '~' + channelNameLower, + escapeHtml(channelNamesMap[channelNameLower].display_name)); + return alias + suffix; + } + } else { + // If the last character is not punctuation, no point in going any further + break; + } + } + + return fullMatch; + } + + let output = text; + output = output.replace(/\B(~([a-z0-9.\-_]*))/gi, replaceChannelMentionWithToken); + + return output; +} + +export function escapeRegex(text) { + if (text == null) { + return ''; + } + return text.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'); +} + +const htmlEntities = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', +}; + +export function escapeHtml(text) { + return text.replace(/[&<>"']/g, (match) => htmlEntities[match]); +} + +export function convertEntityToCharacter(text) { + return text. + replace(/</g, '<'). + replace(/>/g, '>'). + replace(/'/g, '\''). + replace(/"/g, '"'). + replace(/&/g, '&'); +} + +function autolinkHashtags(text, tokens, minimumHashtagLength = 3) { + let output = text; + + var newTokens = new Map(); + for (const [alias, token] of tokens) { + if (token.originalText.lastIndexOf('#', 0) === 0) { + const index = tokens.size + newTokens.size; + const newAlias = `$MM_HASHTAG${index}$`; + + newTokens.set(newAlias, { + value: `${token.originalText}`, + originalText: token.originalText, + hashtag: token.originalText.substring(1), + }); + + output = output.replace(alias, newAlias); + } + } + + // the new tokens are stashed in a separate map since we can't add objects to a map during iteration + for (const newToken of newTokens) { + tokens.set(newToken[0], newToken[1]); + } + + // look for hashtags in the text + function replaceHashtagWithToken(fullMatch, prefix, originalText) { + const index = tokens.size; + const alias = `$MM_HASHTAG${index}$`; + + if (originalText.length < minimumHashtagLength + 1) { + // too short to be a hashtag + return fullMatch; + } + + tokens.set(alias, { + value: `${originalText}`, + originalText, + hashtag: originalText.substring(1), + }); + + return prefix + alias; + } + + return output.replace(XRegExp.cache('(^|\\W)(#\\pL[\\pL\\d\\-_.]*[\\pL\\d])', 'g'), replaceHashtagWithToken); +} + +export function replaceTokens(text, tokens) { + let output = text; + + // iterate backwards through the map so that we do replacement in the opposite order that we added tokens + const aliases = [...tokens.keys()]; + for (let i = aliases.length - 1; i >= 0; i--) { + const alias = aliases[i]; + const token = tokens.get(alias); + output = output.replace(alias, token.value); + } + + return output; +} + +function replaceNewlines(text) { + return text.replace(/\n/g, ' '); +} \ No newline at end of file diff --git a/webapp/src/utils/url.jsx b/webapp/src/utils/url.jsx new file mode 100644 index 00000000..20e3c626 --- /dev/null +++ b/webapp/src/utils/url.jsx @@ -0,0 +1,24 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +export function isUrlSafe(url) { + let unescaped; + + try { + unescaped = decodeURIComponent(url); + } catch (e) { + unescaped = unescape(url); + } + + unescaped = unescaped.replace(/[^\w:]/g, '').toLowerCase(); + + return !unescaped.startsWith('javascript:') && // eslint-disable-line no-script-url + !unescaped.startsWith('vbscript:') && + !unescaped.startsWith('data:'); +} + +export function getScheme(url) { + const match = (/([a-z0-9+.-]+):/i).exec(url); + + return match && match[1]; +} \ No newline at end of file From 7f095437d212e7cca4985156889a3cc400eaf388 Mon Sep 17 00:00:00 2001 From: Daniel Espino Date: Fri, 10 Jan 2020 06:15:09 -0500 Subject: [PATCH 2/6] Remove unneeded dependency --- webapp/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/webapp/package.json b/webapp/package.json index 98c0292a..ad6dbe65 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -40,7 +40,6 @@ "html-to-react": "^1.4.2", "marked": "^0.8.0", "mattermost-redux": "5.14.0", - "mattermost-webapp": "5.14.0", "react": "16.8.6", "react-custom-scrollbars": "^4.2.1", "react-redux": "5.0.7", From 1043200dc5a2a34555c012e69139586d9b8f7378 Mon Sep 17 00:00:00 2001 From: Daniel Espino Date: Fri, 10 Jan 2020 06:33:07 -0500 Subject: [PATCH 3/6] Fix lint errors --- webapp/src/components/sidebar_right/todo_items.jsx | 3 ++- webapp/src/utils/markdown/link_only_renderer.jsx | 11 +++-------- .../src/utils/markdown/link_only_renderer.test.jsx | 12 +++++++----- webapp/src/utils/markdown/remove_markdown.test.js | 8 +++++--- webapp/src/utils/markdown/renderer.jsx | 5 ----- webapp/src/utils/message_html_to_component.jsx | 2 +- 6 files changed, 18 insertions(+), 23 deletions(-) diff --git a/webapp/src/components/sidebar_right/todo_items.jsx b/webapp/src/components/sidebar_right/todo_items.jsx index 24362648..bac5db8d 100644 --- a/webapp/src/components/sidebar_right/todo_items.jsx +++ b/webapp/src/components/sidebar_right/todo_items.jsx @@ -5,6 +5,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import {makeStyleFromTheme, changeOpacity} from 'mattermost-redux/utils/theme_utils'; + import messageHtmlToComponent from 'utils/message_html_to_component'; import * as TextFormatting from 'utils/text_formatting.jsx'; @@ -25,7 +26,7 @@ function ToDoItems(props) { const formattedDate = month + ' ' + day + ', ' + year; const htmlFormattedText = TextFormatting.formatText(item.message); - const itemComponent = messageHtmlToComponent(htmlFormattedText, true); + const itemComponent = messageHtmlToComponent(htmlFormattedText); return (

    { }, ]; - const linkOnlyRenderer = new LinkOnlyRenderer(); - - testCases.forEach((testCase) => it(testCase.description, () => { - expect(formatWithRenderer(testCase.inputText, linkOnlyRenderer)).toEqual(testCase.outputText); - })); + testCases.forEach(testSingleCase); }); + +const linkOnlyRenderer = new LinkOnlyRenderer(); + +const testSingleCase = (testCase) => it(testCase.description, () => { + expect(formatWithRenderer(testCase.inputText, linkOnlyRenderer)).toEqual(testCase.outputText); +}); \ No newline at end of file diff --git a/webapp/src/utils/markdown/remove_markdown.test.js b/webapp/src/utils/markdown/remove_markdown.test.js index aa2bcb3e..2d709d49 100644 --- a/webapp/src/utils/markdown/remove_markdown.test.js +++ b/webapp/src/utils/markdown/remove_markdown.test.js @@ -291,7 +291,9 @@ describe('stripMarkdown | RemoveMarkdown', () => { }, ]; - testCases.forEach((testCase) => it(testCase.description, () => { - expect(stripMarkdown(testCase.inputText)).toEqual(testCase.outputText); - })); + testCases.forEach(testSingleCase); }); + +const testSingleCase = (testCase) => it(testCase.description, () => { + expect(stripMarkdown(testCase.inputText)).toEqual(testCase.outputText); +}); \ No newline at end of file diff --git a/webapp/src/utils/markdown/renderer.jsx b/webapp/src/utils/markdown/renderer.jsx index e29f18dc..b2d93dbc 100644 --- a/webapp/src/utils/markdown/renderer.jsx +++ b/webapp/src/utils/markdown/renderer.jsx @@ -35,19 +35,14 @@ export default class Renderer extends marked.Renderer { className += ' post-code--wrap'; codeClassName = 'hljs'; - let header = ''; - // 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 = code; - let searchedContent = ''; return ( '
    ' + - header + '' + - searchedContent + content + '' + '
    ' diff --git a/webapp/src/utils/message_html_to_component.jsx b/webapp/src/utils/message_html_to_component.jsx index 7eb1c48e..f433007b 100644 --- a/webapp/src/utils/message_html_to_component.jsx +++ b/webapp/src/utils/message_html_to_component.jsx @@ -7,7 +7,7 @@ import {Parser, ProcessNodeDefinitions} from 'html-to-react'; /* * Converts HTML to React components using html-to-react. */ -export function messageHtmlToComponent(html, isRHS, options = {}) { +export function messageHtmlToComponent(html) { if (!html) { return null; } From 9d0ee675a977b6ebd970c535ee97c488c3f39b75 Mon Sep 17 00:00:00 2001 From: Daniel Espino Date: Fri, 10 Jan 2020 06:48:36 -0500 Subject: [PATCH 4/6] Remove tests for markdown --- webapp/src/utils/markdown/index.test.js | 33 -- .../markdown/link_only_renderer.test.jsx | 295 ----------------- .../utils/markdown/remove_markdown.test.js | 299 ------------------ 3 files changed, 627 deletions(-) delete mode 100644 webapp/src/utils/markdown/index.test.js delete mode 100644 webapp/src/utils/markdown/link_only_renderer.test.jsx delete mode 100644 webapp/src/utils/markdown/remove_markdown.test.js diff --git a/webapp/src/utils/markdown/index.test.js b/webapp/src/utils/markdown/index.test.js deleted file mode 100644 index 17b8afb6..00000000 --- a/webapp/src/utils/markdown/index.test.js +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {format} from './index'; - -describe('format', () => { - test('should highlight code without space before language', () => { - const output = format(`~~~diff -- something -+ something else -~~~`); - - expect(output).toContain('Diff'); - expect(output).toContain(''); - }); - - test('should highlight code with space before language', () => { - const output = format(`~~~ diff -- something -+ something else -~~~`); - - expect(output).toContain('Diff'); - expect(output).toContain(''); - }); - - test('should not highlight code with an invalid language', () => { - const output = format(`~~~garbage -~~~`); - - expect(output).not.toContain(''); - }); -}); diff --git a/webapp/src/utils/markdown/link_only_renderer.test.jsx b/webapp/src/utils/markdown/link_only_renderer.test.jsx deleted file mode 100644 index e542d52b..00000000 --- a/webapp/src/utils/markdown/link_only_renderer.test.jsx +++ /dev/null @@ -1,295 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import LinkOnlyRenderer from 'utils/markdown/link_only_renderer'; -import {formatWithRenderer} from 'utils/markdown'; - -describe('formatWithRenderer | LinkOnlyRenderer', () => { - const testCases = [ - { - description: 'emoji: same', - inputText: 'Hey :smile: :+1: :)', - outputText: 'Hey :smile: :+1: :)', - }, - { - description: 'at-mention: same', - inputText: 'Hey @user and @test', - outputText: 'Hey @user and @test', - }, - { - description: 'channel-link: same', - inputText: 'join ~channelname', - outputText: 'join ~channelname', - }, - { - description: 'codespan: single backtick', - inputText: '`single backtick`', - outputText: 'single backtick', - }, - { - description: 'codespan: double backtick', - inputText: '``double backtick``', - outputText: 'double backtick', - }, - { - description: 'codespan: triple backtick', - inputText: '```triple backtick```', - outputText: 'triple backtick', - }, - { - description: 'codespan: inline code', - inputText: 'Inline `code` has ``double backtick`` and ```triple backtick``` around it.', - outputText: 'Inline code has double backtick and triple backtick around it.', - }, - { - description: 'code block: single line code block', - inputText: 'Code block\n```\nline\n```', - outputText: 'Code block line', - }, - { - description: 'code block: multiline code block 2', - inputText: 'Multiline\n```function(number) {\n return number + 1;\n}```', - outputText: 'Multiline function(number) { return number + 1; }', - }, - { - description: 'code block: language highlighting', - inputText: '```javascript\nvar s = "JavaScript syntax highlighting";\nalert(s);\n```', - outputText: 'var s = "JavaScript syntax highlighting"; alert(s);', - }, - { - description: 'blockquote:', - inputText: '> Hey quote', - outputText: 'Hey quote', - }, - { - description: 'blockquote: multiline', - inputText: '> Hey quote.\n> Hello quote.', - outputText: 'Hey quote. Hello quote.', - }, - { - description: 'heading: # H1 header', - inputText: '# H1 header', - outputText: 'H1 header', - }, - { - description: 'heading: heading with @user', - inputText: '# H1 @user', - outputText: 'H1 @user', - }, - { - description: 'heading: ## H2 header', - inputText: '## H2 header', - outputText: 'H2 header', - }, - { - description: 'heading: ### H3 header', - inputText: '### H3 header', - outputText: 'H3 header', - }, - { - description: 'heading: #### H4 header', - inputText: '#### H4 header', - outputText: 'H4 header', - }, - { - description: 'heading: ##### H5 header', - inputText: '##### H5 header', - outputText: 'H5 header', - }, - { - description: 'heading: ###### H6 header', - inputText: '###### H6 header', - outputText: 'H6 header', - }, - { - description: 'heading: multiline with header and paragraph', - inputText: '###### H6 header\nThis is next line.\nAnother line.', - outputText: 'H6 header This is next line. Another line.', - }, - { - description: 'heading: multiline with header and list items', - inputText: '###### H6 header\n- list item 1\n- list item 2', - outputText: 'H6 header list item 1 list item 2', - }, - { - description: 'heading: multiline with header and links', - inputText: '###### H6 header\n[link 1](https://mattermost.com) - [link 2](https://mattermost.com)', - outputText: 'H6 header ' + - 'link 1 - link 2', - }, - { - description: 'list: 1. First ordered list item', - inputText: '1. First ordered list item', - outputText: 'First ordered list item', - }, - { - description: 'list: 2. Another item', - inputText: '1. 2. Another item', - outputText: 'Another item', - }, - { - description: 'list: * Unordered sub-list.', - inputText: '* Unordered sub-list.', - outputText: 'Unordered sub-list.', - }, - { - description: 'list: - Or minuses', - inputText: '- Or minuses', - outputText: 'Or minuses', - }, - { - description: 'list: + Or pluses', - inputText: '+ Or pluses', - outputText: 'Or pluses', - }, - { - description: 'list: multiline', - inputText: '1. First ordered list item\n2. Another item', - outputText: 'First ordered list item Another item', - }, - { - description: 'tablerow:)', - inputText: 'Markdown | Less | Pretty\n' + - '--- | --- | ---\n' + - '*Still* | `renders` | **nicely**\n' + - '1 | 2 | 3\n', - outputText: '', - }, - { - description: 'table:', - inputText: '| Tables | Are | Cool |\n' + - '| ------------- |:-------------:| -----:|\n' + - '| col 3 is | right-aligned | $1600 |\n' + - '| col 2 is | centered | $12 |\n' + - '| zebra stripes | are neat | $1 |\n', - outputText: '', - }, - { - description: 'strong: Bold with **asterisks** or __underscores__.', - inputText: 'Bold with **asterisks** or __underscores__.', - outputText: 'Bold with asterisks or underscores.', - }, - { - description: 'strong & em: Bold and italics with **asterisks and _underscores_**.', - inputText: 'Bold and italics with **asterisks and _underscores_**.', - outputText: 'Bold and italics with asterisks and underscores.', - }, - { - description: 'em: Italics with *asterisks* or _underscores_.', - inputText: 'Italics with *asterisks* or _underscores_.', - outputText: 'Italics with asterisks or underscores.', - }, - { - description: 'del: Strikethrough ~~strike this.~~', - inputText: 'Strikethrough ~~strike this.~~', - outputText: 'Strikethrough strike this.', - }, - { - description: 'links: [inline-style link](http://localhost:8065)', - inputText: '[inline-style link](http://localhost:8065)', - outputText: '' + - 'inline-style link', - }, - { - description: 'image: ![image link](http://localhost:8065/image)', - inputText: '![image link](http://localhost:8065/image)', - outputText: 'image link', - }, - { - description: 'text: plain', - inputText: 'This is plain text.', - outputText: 'This is plain text.', - }, - { - description: 'text: multiline', - inputText: 'This is multiline text.\nHere is the next line.\n', - outputText: 'This is multiline text. Here is the next line.', - }, - { - description: 'text: & entity', - inputText: 'you & me', - outputText: 'you & me', - }, - { - description: 'text: < entity', - inputText: '1<2', - outputText: '1<2', - }, - { - description: 'text: > entity', - inputText: '2>1', - outputText: '2>1', - }, - { - description: 'text: ' entity', - inputText: 'he\'s out', - outputText: 'he's out', - }, - { - description: 'text: " entity', - inputText: 'That is "unique"', - outputText: 'That is "unique"', - }, - { - description: 'text: multiple entities', - inputText: '&<>\'"', - outputText: '&<>'"', - }, - { - description: 'text: multiple entities', - inputText: '"\'><&', - outputText: '"'><&', - }, - { - description: 'text: multiple entities', - inputText: '&<>'"', - outputText: '&<>'"', - }, - { - description: 'text: multiple entities', - inputText: '"'><&', - outputText: '"'><&', - }, - { - description: 'text: multiple entities', - inputText: '&lt;', - outputText: '&lt;', - }, - { - description: 'text: empty string', - inputText: '', - outputText: '', - }, - { - description: 'link: link without a scheme', - inputText: 'Do you like www.mattermost.com?', - outputText: 'Do you like ' + - 'www.mattermost.com?', - }, - { - description: 'link: link with a scheme', - inputText: 'Do you like http://www.mattermost.com?', - outputText: 'Do you like ' + - 'http://www.mattermost.com?', - }, - { - description: 'link: link with a title', - inputText: 'Do you like [Mattermost](http://www.mattermost.com)?', - outputText: 'Do you like ' + - 'Mattermost?', - }, - { - description: 'link: link with curly brackets', - inputText: 'Let\'s try http://example/result?things={stuff}', - outputText: 'Let's try http://example/result?things={stuff}', - }, - ]; - - testCases.forEach(testSingleCase); -}); - -const linkOnlyRenderer = new LinkOnlyRenderer(); - -const testSingleCase = (testCase) => it(testCase.description, () => { - expect(formatWithRenderer(testCase.inputText, linkOnlyRenderer)).toEqual(testCase.outputText); -}); \ No newline at end of file diff --git a/webapp/src/utils/markdown/remove_markdown.test.js b/webapp/src/utils/markdown/remove_markdown.test.js deleted file mode 100644 index 2d709d49..00000000 --- a/webapp/src/utils/markdown/remove_markdown.test.js +++ /dev/null @@ -1,299 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; - -import {stripMarkdown} from 'utils/markdown'; - -describe('stripMarkdown | RemoveMarkdown', () => { - const testCases = [ - { - description: 'emoji: same', - inputText: 'Hey :smile: :+1: :)', - outputText: 'Hey :smile: :+1: :)', - }, - { - description: 'at-mention: same', - inputText: 'Hey @user and @test', - outputText: 'Hey @user and @test', - }, - { - description: 'channel-link: same', - inputText: 'join ~channelname', - outputText: 'join ~channelname', - }, - { - description: 'codespan: single backtick', - inputText: '`single backtick`', - outputText: 'single backtick', - }, - { - description: 'codespan: double backtick', - inputText: '``double backtick``', - outputText: 'double backtick', - }, - { - description: 'codespan: triple backtick', - inputText: '```triple backtick```', - outputText: 'triple backtick', - }, - { - description: 'codespan: inline code', - inputText: 'Inline `code` has ``double backtick`` and ```triple backtick``` around it.', - outputText: 'Inline code has double backtick and triple backtick around it.', - }, - { - description: 'code block: single line code block', - inputText: 'Code block\n```\nline\n```', - outputText: 'Code block line', - }, - { - description: 'code block: multiline code block 2', - inputText: 'Multiline\n```function(number) {\n return number + 1;\n}```', - outputText: 'Multiline function(number) { return number + 1; }', - }, - { - description: 'code block: language highlighting', - inputText: '```javascript\nvar s = "JavaScript syntax highlighting";\nalert(s);\n```', - outputText: 'var s = "JavaScript syntax highlighting"; alert(s);', - }, - { - description: 'blockquote:', - inputText: '> Hey quote', - outputText: 'Hey quote', - }, - { - description: 'blockquote: multiline', - inputText: '> Hey quote.\n> Hello quote.', - outputText: 'Hey quote. Hello quote.', - }, - { - description: 'heading: # H1 header', - inputText: '# H1 header', - outputText: 'H1 header', - }, - { - description: 'heading: heading with @user', - inputText: '# H1 @user', - outputText: 'H1 @user', - }, - { - description: 'heading: ## H2 header', - inputText: '## H2 header', - outputText: 'H2 header', - }, - { - description: 'heading: ### H3 header', - inputText: '### H3 header', - outputText: 'H3 header', - }, - { - description: 'heading: #### H4 header', - inputText: '#### H4 header', - outputText: 'H4 header', - }, - { - description: 'heading: ##### H5 header', - inputText: '##### H5 header', - outputText: 'H5 header', - }, - { - description: 'heading: ###### H6 header', - inputText: '###### H6 header', - outputText: 'H6 header', - }, - { - description: 'heading: multiline with header and paragraph', - inputText: '###### H6 header\nThis is next line.\nAnother line.', - outputText: 'H6 header This is next line. Another line.', - }, - { - description: 'heading: multiline with header and list items', - inputText: '###### H6 header\n- list item 1\n- list item 2', - outputText: 'H6 header list item 1 list item 2', - }, - { - description: 'heading: multiline with header and links', - inputText: '###### H6 header\n[link 1](https://mattermost.com) - [link 2](https://mattermost.com)', - outputText: 'H6 header link 1 - link 2', - }, - { - description: 'list: 1. First ordered list item', - inputText: '1. First ordered list item', - outputText: 'First ordered list item', - }, - { - description: 'list: 2. Another item', - inputText: '1. 2. Another item', - outputText: 'Another item', - }, - { - description: 'list: * Unordered sub-list.', - inputText: '* Unordered sub-list.', - outputText: 'Unordered sub-list.', - }, - { - description: 'list: - Or minuses', - inputText: '- Or minuses', - outputText: 'Or minuses', - }, - { - description: 'list: + Or pluses', - inputText: '+ Or pluses', - outputText: 'Or pluses', - }, - { - description: 'list: multiline', - inputText: '1. First ordered list item\n2. Another item', - outputText: 'First ordered list item Another item', - }, - { - description: 'tablerow:)', - inputText: 'Markdown | Less | Pretty\n' + - '--- | --- | ---\n' + - '*Still* | `renders` | **nicely**\n' + - '1 | 2 | 3\n', - outputText: '', - }, - { - description: 'table:', - inputText: '| Tables | Are | Cool |\n' + - '| ------------- |:-------------:| -----:|\n' + - '| col 3 is | right-aligned | $1600 |\n' + - '| col 2 is | centered | $12 |\n' + - '| zebra stripes | are neat | $1 |\n', - outputText: '', - }, - { - description: 'strong: Bold with **asterisks** or __underscores__.', - inputText: 'Bold with **asterisks** or __underscores__.', - outputText: 'Bold with asterisks or underscores.', - }, - { - description: 'strong & em: Bold and italics with **asterisks and _underscores_**.', - inputText: 'Bold and italics with **asterisks and _underscores_**.', - outputText: 'Bold and italics with asterisks and underscores.', - }, - { - description: 'em: Italics with *asterisks* or _underscores_.', - inputText: 'Italics with *asterisks* or _underscores_.', - outputText: 'Italics with asterisks or underscores.', - }, - { - description: 'del: Strikethrough ~~strike this.~~', - inputText: 'Strikethrough ~~strike this.~~', - outputText: 'Strikethrough strike this.', - }, - { - description: 'links: [inline-style link](http://localhost:8065)', - inputText: '[inline-style link](http://localhost:8065)', - outputText: 'inline-style link', - }, - { - description: 'image: ![image link](http://localhost:8065/image)', - inputText: '![image link](http://localhost:8065/image)', - outputText: 'image link', - }, - { - description: 'text: plain', - inputText: 'This is plain text.', - outputText: 'This is plain text.', - }, - { - description: 'text: multiline', - inputText: 'This is multiline text.\nHere is the next line.\n', - outputText: 'This is multiline text. Here is the next line.', - }, - { - description: 'text: multiline with blockquote', - inputText: 'This is multiline text.\n> With quote', - outputText: 'This is multiline text. With quote', - }, - { - description: 'text: multiline with list items', - inputText: 'This is multiline text.\n * List item ', - outputText: 'This is multiline text. List item', - }, - { - description: 'text: & entity', - inputText: 'you & me', - outputText: 'you & me', - }, - { - description: 'text: < entity', - inputText: '1<2', - outputText: '1<2', - }, - { - description: 'text: > entity', - inputText: '2>1', - outputText: '2>1', - }, - { - description: 'text: ' entity', - inputText: 'he\'s out', - outputText: 'he\'s out', - }, - { - description: 'text: " entity', - inputText: 'That is "unique"', - outputText: 'That is "unique"', - }, - { - description: 'text: multiple entities', - inputText: '&<>\'"', - outputText: '&<>\'"', - }, - { - description: 'text: multiple entities', - inputText: '"\'><&', - outputText: '"\'><&', - }, - { - description: 'text: multiple entities', - inputText: '&<>'"', - outputText: '&<>\'"', - }, - { - description: 'text: multiple entities', - inputText: '"'><&', - outputText: '"\'><&', - }, - { - description: 'text: multiple entities', - inputText: '&lt;', - outputText: '<', - }, - { - description: 'text: empty string', - inputText: '', - outputText: '', - }, - { - description: 'text: null', - inputText: null, - outputText: null, - }, - { - description: 'text: {}', - inputText: {key: 'value'}, - outputText: {key: 'value'}, - }, - { - description: 'text: []', - inputText: [1], - outputText: [1], - }, - { - description: 'text: node', - inputText: (
    ), - outputText: (
    ), - }, - ]; - - testCases.forEach(testSingleCase); -}); - -const testSingleCase = (testCase) => it(testCase.description, () => { - expect(stripMarkdown(testCase.inputText)).toEqual(testCase.outputText); -}); \ No newline at end of file From ba612c04f62179c8bb998f38a861532be1883074 Mon Sep 17 00:00:00 2001 From: Daniel Espino Date: Fri, 10 Jan 2020 07:51:47 -0500 Subject: [PATCH 5/6] Use exported PostUtils from webapp for markdown --- webapp/package-lock.json | 98 ------- webapp/package.json | 5 +- .../components/sidebar_right/todo_items.jsx | 7 +- webapp/src/utils/markdown/index.js | 35 --- .../src/utils/markdown/link_only_renderer.jsx | 26 -- .../utils/markdown/mentionable_renderer.jsx | 81 ------ webapp/src/utils/markdown/remove_markdown.js | 78 ----- webapp/src/utils/markdown/renderer.jsx | 200 ------------- .../src/utils/message_html_to_component.jsx | 45 --- webapp/src/utils/text_formatting.jsx | 268 ------------------ webapp/src/utils/url.jsx | 24 -- 11 files changed, 4 insertions(+), 863 deletions(-) delete mode 100644 webapp/src/utils/markdown/index.js delete mode 100644 webapp/src/utils/markdown/link_only_renderer.jsx delete mode 100644 webapp/src/utils/markdown/mentionable_renderer.jsx delete mode 100644 webapp/src/utils/markdown/remove_markdown.js delete mode 100644 webapp/src/utils/markdown/renderer.jsx delete mode 100644 webapp/src/utils/message_html_to_component.jsx delete mode 100644 webapp/src/utils/text_formatting.jsx delete mode 100644 webapp/src/utils/url.jsx diff --git a/webapp/package-lock.json b/webapp/package-lock.json index c4c8c53d..9d013000 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -3217,22 +3217,6 @@ } } }, - "@babel/runtime-corejs2": { - "version": "7.7.7", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs2/-/runtime-corejs2-7.7.7.tgz", - "integrity": "sha512-P91T3dFYQL7aj44PxOMIAbo66Ag3NbmXG9fseSYaXxapp3K9XTct5HU9IpTOm2D0AoktKusgqzN5YcSxZXEKBQ==", - "requires": { - "core-js": "^2.6.5", - "regenerator-runtime": "^0.13.2" - }, - "dependencies": { - "regenerator-runtime": { - "version": "0.13.3", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz", - "integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==" - } - } - }, "@babel/template": { "version": "7.4.4", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.4.4.tgz", @@ -5277,26 +5261,12 @@ "@babel/runtime": "^7.1.2" } }, - "dom-serializer": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", - "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", - "requires": { - "domelementtype": "^2.0.1", - "entities": "^2.0.0" - } - }, "domain-browser": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", "dev": true }, - "domelementtype": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz", - "integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==" - }, "domexception": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", @@ -5306,24 +5276,6 @@ "webidl-conversions": "^4.0.2" } }, - "domhandler": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.0.0.tgz", - "integrity": "sha512-eKLdI5v9m67kbXQbJSNn1zjh0SDzvzWVWtX+qEI3eMjZw8daH9k8rlj1FZY9memPwjiskQFbe7vHVVJIAqoEhw==", - "requires": { - "domelementtype": "^2.0.1" - } - }, - "domutils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.0.0.tgz", - "integrity": "sha512-n5SelJ1axbO636c2yUtOGia/IcJtVtlhQbFiVDBZHKV5ReJO1ViX7sFEemtuyoAnBxk5meNSYgA8V4s0271efg==", - "requires": { - "dom-serializer": "^0.2.1", - "domelementtype": "^2.0.1", - "domhandler": "^3.0.0" - } - }, "duplexify": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", @@ -5401,11 +5353,6 @@ "tapable": "^0.1.8" } }, - "entities": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz", - "integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==" - }, "errno": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", @@ -7264,28 +7211,6 @@ "whatwg-encoding": "^1.0.1" } }, - "html-to-react": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/html-to-react/-/html-to-react-1.4.2.tgz", - "integrity": "sha512-TdTfxd95sRCo6QL8admCkE7mvNNrXtGoVr1dyS+7uvc8XCqAymnf/6ckclvnVbQNUo2Nh21VPwtfEHd0khiV7g==", - "requires": { - "domhandler": "^3.0", - "htmlparser2": "^4.0", - "lodash.camelcase": "^4.3.0", - "ramda": "^0.26" - } - }, - "htmlparser2": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-4.0.0.tgz", - "integrity": "sha512-cChwXn5Vam57fyXajDtPXL1wTYc8JtLbr2TN76FYu05itVVVealxLowe2B3IEznJG4p9HAYn/0tJaRlGuEglFQ==", - "requires": { - "domelementtype": "^2.0.1", - "domhandler": "^3.0.0", - "domutils": "^2.0.0", - "entities": "^2.0.0" - } - }, "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -8748,11 +8673,6 @@ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.14.tgz", "integrity": "sha512-7zchRrGa8UZXjD/4ivUWP1867jDkhzTG2c/uj739utSd7O/pFFdxspCemIFKEEjErbcqRzn8nKnGsi7mvTgRPA==" }, - "lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" - }, "lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -8861,11 +8781,6 @@ "object-visit": "^1.0.0" } }, - "marked": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-0.8.0.tgz", - "integrity": "sha512-MyUe+T/Pw4TZufHkzAfDj6HarCBWia2y27/bhuYkTaiUnfDYFnCP3KUN+9oM7Wi6JA2rymtVYbQu3spE0GCmxQ==" - }, "mattermost-redux": { "version": "5.14.0", "resolved": "https://registry.npmjs.org/mattermost-redux/-/mattermost-redux-5.14.0.tgz", @@ -10281,11 +10196,6 @@ "performance-now": "^2.1.0" } }, - "ramda": { - "version": "0.26.1", - "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.26.1.tgz", - "integrity": "sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ==" - }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -12703,14 +12613,6 @@ "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", "dev": true }, - "xregexp": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.2.4.tgz", - "integrity": "sha512-sO0bYdYeJAJBcJA8g7MJJX7UrOZIfJPd8U2SC7B2Dd/J24U0aQNoGp33shCaBSWeb0rD5rh6VBUIXOkGal1TZA==", - "requires": { - "@babel/runtime-corejs2": "^7.2.0" - } - }, "xtend": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", diff --git a/webapp/package.json b/webapp/package.json index ad6dbe65..fc696145 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -37,14 +37,11 @@ "webpack-cli": "3.3.5" }, "dependencies": { - "html-to-react": "^1.4.2", - "marked": "^0.8.0", "mattermost-redux": "5.14.0", "react": "16.8.6", "react-custom-scrollbars": "^4.2.1", "react-redux": "5.0.7", "react-transition-group": "4.2.2", - "redux": "4.0.1", - "xregexp": "^4.2.4" + "redux": "4.0.1" } } diff --git a/webapp/src/components/sidebar_right/todo_items.jsx b/webapp/src/components/sidebar_right/todo_items.jsx index bac5db8d..e16667a1 100644 --- a/webapp/src/components/sidebar_right/todo_items.jsx +++ b/webapp/src/components/sidebar_right/todo_items.jsx @@ -6,8 +6,7 @@ import PropTypes from 'prop-types'; import {makeStyleFromTheme, changeOpacity} from 'mattermost-redux/utils/theme_utils'; -import messageHtmlToComponent from 'utils/message_html_to_component'; -import * as TextFormatting from 'utils/text_formatting.jsx'; +const PostUtils = window['PostUtils']; // import the post utilities const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; @@ -25,8 +24,8 @@ function ToDoItems(props) { const formattedTime = hours + ':' + minutes.substr(-2) + ':' + seconds.substr(-2); const formattedDate = month + ' ' + day + ', ' + year; - const htmlFormattedText = TextFormatting.formatText(item.message); - const itemComponent = messageHtmlToComponent(htmlFormattedText); + const htmlFormattedText = PostUtils.formatText(item.message); + const itemComponent = PostUtils.messageHtmlToComponent(htmlFormattedText); return (
    0) { - return convertEntityToCharacter(formatWithRenderer(text, removeMarkdown)).trim(); - } - - return text; -} diff --git a/webapp/src/utils/markdown/link_only_renderer.jsx b/webapp/src/utils/markdown/link_only_renderer.jsx deleted file mode 100644 index 4b927fd1..00000000 --- a/webapp/src/utils/markdown/link_only_renderer.jsx +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {getScheme} from 'utils/url'; - -import RemoveMarkdown from './remove_markdown'; - -export default class LinkOnlyRenderer extends RemoveMarkdown { - link(href, title, text) { - let outHref = href; - - if (!getScheme(href)) { - outHref = `http://${outHref}`; - } - - let output = `${text}`; - - return output; - } -} \ No newline at end of file diff --git a/webapp/src/utils/markdown/mentionable_renderer.jsx b/webapp/src/utils/markdown/mentionable_renderer.jsx deleted file mode 100644 index 44d1bad1..00000000 --- a/webapp/src/utils/markdown/mentionable_renderer.jsx +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import marked from 'marked'; - -/** A Markdown renderer that converts a post into plain text that we can search for mentions */ -export default class MentionableRenderer extends marked.Renderer { - code() { - // Code blocks can't contain mentions - return '\n'; - } - - blockquote(text) { - return text + '\n'; - } - - heading(text) { - return text + '\n'; - } - - hr() { - return '\n'; - } - - list(body) { - return body + '\n'; - } - - listitem(text) { - return text + '\n'; - } - - paragraph(text) { - return text + '\n'; - } - - table(header, body) { - return header + '\n' + body; - } - - tablerow(content) { - return content; - } - - tablecell(content) { - return content + '\n'; - } - - strong(text) { - return ' ' + text + ' '; - } - - em(text) { - return ' ' + text + ' '; - } - - codespan() { - // Code spans can't contain mentions - return ' '; - } - - br() { - return '\n'; - } - - del(text) { - return ' ' + text + ' '; - } - - link(href, title, text) { - return ' ' + text + ' '; - } - - image(href, title, text) { - return ' ' + text + ' '; - } - - text(text) { - return text; - } -} diff --git a/webapp/src/utils/markdown/remove_markdown.js b/webapp/src/utils/markdown/remove_markdown.js deleted file mode 100644 index e2e5bd81..00000000 --- a/webapp/src/utils/markdown/remove_markdown.js +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import marked from 'marked'; - -export default class RemoveMarkdown extends marked.Renderer { - code(text) { - return text.replace(/\n/g, ' '); - } - - blockquote(text) { - return text.replace(/\n/g, ' '); - } - - heading(text) { - return text + ' '; - } - - hr() { - return ''; - } - - list(body) { - return body; - } - - listitem(text) { - return text + ' '; - } - - paragraph(text) { - return text + ' '; - } - - table() { - return ''; - } - - tablerow() { - return ''; - } - - tablecell() { - return ''; - } - - strong(text) { - return text; - } - - em(text) { - return text; - } - - codespan(text) { - return text.replace(/\n/g, ' '); - } - - br() { - return ' '; - } - - del(text) { - return text; - } - - link(href, title, text) { - return text; - } - - image(href, title, text) { - return text; - } - - text(text) { - return text.replace('\n', ' '); - } -} diff --git a/webapp/src/utils/markdown/renderer.jsx b/webapp/src/utils/markdown/renderer.jsx deleted file mode 100644 index b2d93dbc..00000000 --- a/webapp/src/utils/markdown/renderer.jsx +++ /dev/null @@ -1,200 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import marked from 'marked'; - -import * as TextFormatting from 'utils/text_formatting.jsx'; -import {getScheme, isUrlSafe} from 'utils/url.jsx'; - -export default class Renderer 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') { - return `
    `; - } - - // treat html as xml to prevent injection attacks - if (usedLanguage === 'html') { - usedLanguage = 'xml'; - } - - let className = 'post-code'; - let codeClassName = 'hljs hljs-ln'; - className += ' post-code--wrap'; - codeClassName = 'hljs'; - - // 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 = code; - - return ( - '
    ' + - '' + - content + - '' + - '
    ' - ); - } - - codespan(text) { - let output = text; - - if (this.formattingOptions.searchPatterns) { - const tokens = new Map(); - output = TextFormatting.replaceTokens(output, tokens); - } - - return ( - '' + - '' + - output + - '' + - '' - ); - } - - br() { - if (this.formattingOptions.singleline) { - return ' '; - } - - return super.br(); - } - - heading(text, level) { - return `${text}`; - } - - link(href, title, text, isUrl) { - let outHref = href; - - if (!href.startsWith('/')) { - const scheme = getScheme(href); - if (!scheme) { - outHref = `http://${outHref}`; - } else if (isUrl && this.formattingOptions.autolinkedUrlSchemes) { - const isValidUrl = this.formattingOptions.autolinkedUrlSchemes.indexOf(scheme.toLowerCase()) !== -1; - - if (!isValidUrl) { - return text; - } - } - } - - if (!isUrlSafe(unescapeHtmlEntities(href))) { - return text; - } - - let output = ']*>/g, '') + ''; - - return output; - } - - paragraph(text) { - if (this.formattingOptions.singleline) { - let result; - if (text.includes('class="markdown-inline-img"')) { - /* - ** use a div tag instead of a p tag to allow other divs to be nested, - ** which avoids errors of incorrect DOM nesting (
    inside

    ) - */ - result = `

    ${text}
    `; - } else { - result = `

    ${text}

    `; - } - return result; - } - - return super.paragraph(text); - } - - table(header, body) { - return `
    ${header}${body}
    `; - } - - tablerow(content) { - return `${content}`; - } - - tablecell(content, flags) { - return marked.Renderer.prototype.tablecell(content, flags).trim(); - } - - 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); - } -} - -// Marked helper functions that should probably just be exported - -function unescapeHtmlEntities(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 ''; - }); -} diff --git a/webapp/src/utils/message_html_to_component.jsx b/webapp/src/utils/message_html_to_component.jsx deleted file mode 100644 index f433007b..00000000 --- a/webapp/src/utils/message_html_to_component.jsx +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import {Parser, ProcessNodeDefinitions} from 'html-to-react'; - -/* - * Converts HTML to React components using html-to-react. - */ -export function messageHtmlToComponent(html) { - if (!html) { - return null; - } - - const parser = new Parser(); - const processNodeDefinitions = new ProcessNodeDefinitions(React); - - function isValidNode() { - return true; - } - - const processingInstructions = [ - - // Workaround to fix MM-14931 - { - replaceChildren: false, - shouldProcessNode: (node) => node.type === 'tag' && node.name === 'input' && node.attribs.type === 'checkbox', - processNode: (node) => { - const attribs = node.attribs || {}; - node.attribs.checked = Boolean(attribs.checked); - - return React.createElement('input', {...node.attribs}); - }, - }, - ]; - - processingInstructions.push({ - shouldProcessNode: () => true, - processNode: processNodeDefinitions.processDefaultNode, - }); - - return parser.parseWithInstructions(html, isValidNode, processingInstructions); -} - -export default messageHtmlToComponent; diff --git a/webapp/src/utils/text_formatting.jsx b/webapp/src/utils/text_formatting.jsx deleted file mode 100644 index 52cbcd74..00000000 --- a/webapp/src/utils/text_formatting.jsx +++ /dev/null @@ -1,268 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import XRegExp from 'xregexp'; - -import * as Markdown from './markdown'; - -const punctuation = XRegExp.cache('[^\\pL\\d]'); - -const htmlEmojiPattern = /^

    \s*(?:]*>|]*>[^<]*<\/span>\s*|[^<]*<\/span>\s*)+<\/p>$/; - -// Performs formatting of user posts including converting urls, hashtags, -// @mentions and ~channels to links by taking a user's message and returning a string of formatted html. Also takes -// a number of options as part of the second parameter: -// - singleline - Specifies whether or not to remove newlines. Defaults to false. -// - markdown - Enables markdown parsing. Defaults to true. -// - siteURL - The origin of this Mattermost instance. If provided, links to channels and posts will be replaced with internal -// links that can be handled by a special click handler. -// - channelNamesMap - An object mapping channel display names to channels. If provided, ~channel mentions will be replaced with -// links to the relevant channel. -// - team - The current team. -// - minimumHashtagLength - Minimum number of characters in a hashtag. Defaults to 3. -export function formatText(text, inputOptions) { - if (!text || typeof text !== 'string') { - return ''; - } - - let output = text; - const options = Object.assign({}, inputOptions); - - if (!('markdown' in options) || options.markdown) { - // the markdown renderer will call doFormatText as necessary - output = Markdown.format(output, options); - if (output.includes('class="markdown-inline-img"')) { - /* - ** remove p tag to allow other divs to be nested, - ** which allows markdown images to open preview window - */ - const replacer = (match) => { - return match === '

    ' ? '

    ' : '
    '; - }; - output = output.replace(/

    |<\/p>/g, replacer); - } - } else { - output = sanitizeHtml(output); - output = doFormatText(output, options); - } - - // replace newlines with spaces if necessary - if (options.singleline) { - output = replaceNewlines(output); - } - - if (htmlEmojiPattern.test(output.trim())) { - output = '' + output.trim() + ''; - } - - return output; -} - -// Performs most of the actual formatting work for formatText. Not intended to be called normally. -export function doFormatText(text, options) { - let output = text; - - const tokens = new Map(); - - if (options.channelNamesMap) { - output = autolinkChannelMentions(output, tokens, options.channelNamesMap, options.team); - } - - output = autolinkEmails(output, tokens); - output = autolinkHashtags(output, tokens, options.minimumHashtagLength); - - // reinsert tokens with formatted versions of the important words and phrases - output = replaceTokens(output, tokens); - - return output; -} - -export function sanitizeHtml(text) { - let output = text; - - // normal string.replace only does a single occurrance so use a regex instead - output = output.replace(/&/g, '&'); - output = output.replace(//g, '>'); - output = output.replace(/'/g, '''); - output = output.replace(/"/g, '"'); - - return output; -} - -// Copied from our fork of commonmark.js -var emailAlphaNumericChars = '\\p{L}\\p{Nd}'; -var emailSpecialCharacters = '!#$%&\'*+\\-\\/=?^_`{|}~'; -var emailRestrictedSpecialCharacters = '\\s(),:;<>@\\[\\]'; -var emailValidCharacters = emailAlphaNumericChars + emailSpecialCharacters; -var emailValidRestrictedCharacters = emailValidCharacters + emailRestrictedSpecialCharacters; -var emailStartPattern = '(?:[' + emailValidCharacters + '](?:[' + emailValidCharacters + ']|\\.(?!\\.|@))*|\\"[' + emailValidRestrictedCharacters + '.]+\\")@'; -var reEmail = XRegExp.cache('(^|[^\\pL\\d])(' + emailStartPattern + '[\\pL\\d.\\-]+[.]\\pL{2,4}(?=$|[^\\p{L}]))', 'g'); - -// Convert emails into tokens -function autolinkEmails(text, tokens) { - function replaceEmailWithToken(fullMatch, prefix, email) { - const index = tokens.size; - const alias = `$MM_EMAIL${index}$`; - - tokens.set(alias, { - value: `${email}`, - originalText: email, - }); - - return prefix + alias; - } - - let output = text; - output = XRegExp.replace(text, reEmail, replaceEmailWithToken); - - return output; -} - -function autolinkChannelMentions(text, tokens, channelNamesMap, team) { - function channelMentionExists(c) { - return Boolean(channelNamesMap[c]); - } - function addToken(channelName, mention, displayName) { - const index = tokens.size; - const alias = `$MM_CHANNELMENTION${index}$`; - let href = '#'; - if (team) { - href = (window.basename || '') + '/' + team.name + '/channels/' + channelName; - } - - tokens.set(alias, { - value: `~${displayName}`, - originalText: mention, - }); - return alias; - } - - function replaceChannelMentionWithToken(fullMatch, mention, channelName) { - let channelNameLower = channelName.toLowerCase(); - - if (channelMentionExists(channelNameLower)) { - // Exact match - const alias = addToken(channelNameLower, mention, escapeHtml(channelNamesMap[channelNameLower].display_name)); - return alias; - } - - // Not an exact match, attempt to truncate any punctuation to see if we can find a channel - const originalChannelName = channelNameLower; - - for (let c = channelNameLower.length; c > 0; c--) { - if (punctuation.test(channelNameLower[c - 1])) { - channelNameLower = channelNameLower.substring(0, c - 1); - - if (channelMentionExists(channelNameLower)) { - const suffix = originalChannelName.substr(c - 1); - const alias = addToken(channelNameLower, '~' + channelNameLower, - escapeHtml(channelNamesMap[channelNameLower].display_name)); - return alias + suffix; - } - } else { - // If the last character is not punctuation, no point in going any further - break; - } - } - - return fullMatch; - } - - let output = text; - output = output.replace(/\B(~([a-z0-9.\-_]*))/gi, replaceChannelMentionWithToken); - - return output; -} - -export function escapeRegex(text) { - if (text == null) { - return ''; - } - return text.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'); -} - -const htmlEntities = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''', -}; - -export function escapeHtml(text) { - return text.replace(/[&<>"']/g, (match) => htmlEntities[match]); -} - -export function convertEntityToCharacter(text) { - return text. - replace(/</g, '<'). - replace(/>/g, '>'). - replace(/'/g, '\''). - replace(/"/g, '"'). - replace(/&/g, '&'); -} - -function autolinkHashtags(text, tokens, minimumHashtagLength = 3) { - let output = text; - - var newTokens = new Map(); - for (const [alias, token] of tokens) { - if (token.originalText.lastIndexOf('#', 0) === 0) { - const index = tokens.size + newTokens.size; - const newAlias = `$MM_HASHTAG${index}$`; - - newTokens.set(newAlias, { - value: `${token.originalText}`, - originalText: token.originalText, - hashtag: token.originalText.substring(1), - }); - - output = output.replace(alias, newAlias); - } - } - - // the new tokens are stashed in a separate map since we can't add objects to a map during iteration - for (const newToken of newTokens) { - tokens.set(newToken[0], newToken[1]); - } - - // look for hashtags in the text - function replaceHashtagWithToken(fullMatch, prefix, originalText) { - const index = tokens.size; - const alias = `$MM_HASHTAG${index}$`; - - if (originalText.length < minimumHashtagLength + 1) { - // too short to be a hashtag - return fullMatch; - } - - tokens.set(alias, { - value: `${originalText}`, - originalText, - hashtag: originalText.substring(1), - }); - - return prefix + alias; - } - - return output.replace(XRegExp.cache('(^|\\W)(#\\pL[\\pL\\d\\-_.]*[\\pL\\d])', 'g'), replaceHashtagWithToken); -} - -export function replaceTokens(text, tokens) { - let output = text; - - // iterate backwards through the map so that we do replacement in the opposite order that we added tokens - const aliases = [...tokens.keys()]; - for (let i = aliases.length - 1; i >= 0; i--) { - const alias = aliases[i]; - const token = tokens.get(alias); - output = output.replace(alias, token.value); - } - - return output; -} - -function replaceNewlines(text) { - return text.replace(/\n/g, ' '); -} \ No newline at end of file diff --git a/webapp/src/utils/url.jsx b/webapp/src/utils/url.jsx deleted file mode 100644 index 20e3c626..00000000 --- a/webapp/src/utils/url.jsx +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -export function isUrlSafe(url) { - let unescaped; - - try { - unescaped = decodeURIComponent(url); - } catch (e) { - unescaped = unescape(url); - } - - unescaped = unescaped.replace(/[^\w:]/g, '').toLowerCase(); - - return !unescaped.startsWith('javascript:') && // eslint-disable-line no-script-url - !unescaped.startsWith('vbscript:') && - !unescaped.startsWith('data:'); -} - -export function getScheme(url) { - const match = (/([a-z0-9+.-]+):/i).exec(url); - - return match && match[1]; -} \ No newline at end of file From ea795ca267a70a541f6e379c5bdd2af4dfe40b30 Mon Sep 17 00:00:00 2001 From: Daniel Espino Date: Fri, 10 Jan 2020 08:27:11 -0500 Subject: [PATCH 6/6] Fix lint erro --- webapp/src/components/sidebar_right/todo_items.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/components/sidebar_right/todo_items.jsx b/webapp/src/components/sidebar_right/todo_items.jsx index e16667a1..eacf474e 100644 --- a/webapp/src/components/sidebar_right/todo_items.jsx +++ b/webapp/src/components/sidebar_right/todo_items.jsx @@ -6,7 +6,7 @@ import PropTypes from 'prop-types'; import {makeStyleFromTheme, changeOpacity} from 'mattermost-redux/utils/theme_utils'; -const PostUtils = window['PostUtils']; // import the post utilities +const PostUtils = window.PostUtils; // import the post utilities const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];