2016-09-22 19:18:12 +03:00
|
|
|
/*
|
|
|
|
Copyright 2016 OpenMarket Ltd
|
|
|
|
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
you may not use this file except in compliance with the License.
|
|
|
|
You may obtain a copy of the License at
|
|
|
|
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
See the License for the specific language governing permissions and
|
|
|
|
limitations under the License.
|
|
|
|
*/
|
|
|
|
|
2016-11-29 22:56:48 +03:00
|
|
|
import commonmark from 'commonmark';
|
2017-02-02 17:17:07 +03:00
|
|
|
import escape from 'lodash/escape';
|
|
|
|
|
2017-09-13 14:04:46 +03:00
|
|
|
const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u'];
|
2017-02-02 17:17:07 +03:00
|
|
|
|
|
|
|
// These types of node are definitely text
|
|
|
|
const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document'];
|
|
|
|
|
|
|
|
function is_allowed_html_tag(node) {
|
|
|
|
// Regex won't work for tags with attrs, but we only
|
|
|
|
// allow <del> anyway.
|
|
|
|
const matches = /^<\/?(.*)>$/.exec(node.literal);
|
|
|
|
if (matches && matches.length == 2) {
|
|
|
|
const tag = matches[1];
|
|
|
|
return ALLOWED_HTML_TAGS.indexOf(tag) > -1;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
function html_if_tag_allowed(node) {
|
|
|
|
if (is_allowed_html_tag(node)) {
|
|
|
|
this.lit(node.literal);
|
|
|
|
return;
|
|
|
|
} else {
|
|
|
|
this.lit(escape(node.literal));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Returns true if the parse output containing the node
|
|
|
|
* comprises multiple block level elements (ie. lines),
|
|
|
|
* or false if it is only a single line.
|
|
|
|
*/
|
|
|
|
function is_multi_line(node) {
|
2017-10-11 19:56:17 +03:00
|
|
|
let par = node;
|
2017-02-02 17:17:07 +03:00
|
|
|
while (par.parent) {
|
|
|
|
par = par.parent;
|
|
|
|
}
|
|
|
|
return par.firstChild != par.lastChild;
|
|
|
|
}
|
2016-09-22 19:18:12 +03:00
|
|
|
|
2017-08-03 20:21:08 +03:00
|
|
|
import linkifyMatrix from './linkify-matrix';
|
|
|
|
import * as linkify from 'linkifyjs';
|
|
|
|
linkifyMatrix(linkify);
|
|
|
|
|
|
|
|
// Thieved from draft-js-export-markdown
|
|
|
|
function escapeMarkdown(s) {
|
|
|
|
return s.replace(/[*_`]/g, '\\$&');
|
|
|
|
}
|
|
|
|
|
|
|
|
// Replace URLs, room aliases and user IDs with md-escaped URLs
|
|
|
|
function linkifyMarkdown(s) {
|
|
|
|
const links = linkify.find(s);
|
|
|
|
links.forEach((l) => {
|
|
|
|
// This may replace several instances of `l.value` at once, but that's OK
|
|
|
|
s = s.replace(l.value, escapeMarkdown(l.value));
|
|
|
|
});
|
|
|
|
return s;
|
|
|
|
}
|
|
|
|
|
2016-09-22 19:18:12 +03:00
|
|
|
/**
|
2017-02-02 14:27:07 +03:00
|
|
|
* Class that wraps commonmark, adding the ability to see whether
|
2016-09-22 19:18:12 +03:00
|
|
|
* a given message actually uses any markdown syntax or whether
|
|
|
|
* it's plain text.
|
|
|
|
*/
|
|
|
|
export default class Markdown {
|
|
|
|
constructor(input) {
|
2017-08-03 20:21:08 +03:00
|
|
|
this.input = linkifyMarkdown(input);
|
2017-02-02 14:34:39 +03:00
|
|
|
|
|
|
|
const parser = new commonmark.Parser();
|
|
|
|
this.parsed = parser.parse(this.input);
|
2016-09-22 19:18:12 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
isPlainText() {
|
2017-02-02 17:17:07 +03:00
|
|
|
const walker = this.parsed.walker();
|
|
|
|
|
|
|
|
let ev;
|
|
|
|
while ( (ev = walker.next()) ) {
|
|
|
|
const node = ev.node;
|
|
|
|
if (TEXT_NODES.indexOf(node.type) > -1) {
|
|
|
|
// definitely text
|
|
|
|
continue;
|
|
|
|
} else if (node.type == 'html_inline' || node.type == 'html_block') {
|
|
|
|
// if it's an allowed html tag, we need to render it and therefore
|
|
|
|
// we will need to use HTML. If it's not allowed, it's not HTML since
|
|
|
|
// we'll just be treating it as text.
|
|
|
|
if (is_allowed_html_tag(node)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return false;
|
|
|
|
}
|
2016-09-22 19:18:12 +03:00
|
|
|
}
|
2017-02-02 17:17:07 +03:00
|
|
|
return true;
|
2016-09-22 19:18:12 +03:00
|
|
|
}
|
|
|
|
|
2017-01-19 13:55:36 +03:00
|
|
|
toHTML() {
|
2017-02-17 21:06:00 +03:00
|
|
|
const renderer = new commonmark.HtmlRenderer({
|
|
|
|
safe: false,
|
|
|
|
|
|
|
|
// Set soft breaks to hard HTML breaks: commonmark
|
|
|
|
// puts softbreaks in for multiple lines in a blockquote,
|
|
|
|
// so if these are just newline characters then the
|
|
|
|
// block quote ends up all on one line
|
|
|
|
// (https://github.com/vector-im/riot-web/issues/3154)
|
|
|
|
softbreak: '<br />',
|
|
|
|
});
|
2017-02-02 14:45:21 +03:00
|
|
|
const real_paragraph = renderer.paragraph;
|
2017-01-18 21:29:11 +03:00
|
|
|
|
2017-02-02 14:45:21 +03:00
|
|
|
renderer.paragraph = function(node, entering) {
|
2017-01-18 21:29:11 +03:00
|
|
|
// If there is only one top level node, just return the
|
|
|
|
// bare text: it's a single line of text and so should be
|
|
|
|
// 'inline', rather than unnecessarily wrapped in its own
|
|
|
|
// p tag. If, however, we have multiple nodes, each gets
|
|
|
|
// its own p tag to keep them as separate paragraphs.
|
2017-02-02 17:17:07 +03:00
|
|
|
if (is_multi_line(node)) {
|
2017-01-18 21:29:11 +03:00
|
|
|
real_paragraph.call(this, node, entering);
|
2017-01-18 00:20:05 +03:00
|
|
|
}
|
2017-01-20 17:22:27 +03:00
|
|
|
};
|
2017-01-18 00:20:05 +03:00
|
|
|
|
2017-02-02 17:17:07 +03:00
|
|
|
renderer.html_inline = html_if_tag_allowed;
|
|
|
|
renderer.html_block = function(node) {
|
|
|
|
// as with `paragraph`, we only insert line breaks
|
|
|
|
// if there are multiple lines in the markdown.
|
|
|
|
const isMultiLine = is_multi_line(node);
|
|
|
|
|
|
|
|
if (isMultiLine) this.cr();
|
|
|
|
html_if_tag_allowed.call(this, node);
|
|
|
|
if (isMultiLine) this.cr();
|
2017-10-11 19:56:17 +03:00
|
|
|
};
|
2017-02-02 17:17:07 +03:00
|
|
|
|
2017-02-02 14:45:21 +03:00
|
|
|
return renderer.render(this.parsed);
|
2017-01-18 21:29:11 +03:00
|
|
|
}
|
|
|
|
|
2017-02-02 14:27:07 +03:00
|
|
|
/*
|
2017-02-02 17:17:07 +03:00
|
|
|
* Render the markdown message to plain text. That is, essentially
|
2017-02-02 14:27:07 +03:00
|
|
|
* just remove any backslashes escaping what would otherwise be
|
|
|
|
* markdown syntax
|
|
|
|
* (to fix https://github.com/vector-im/riot-web/issues/2870)
|
|
|
|
*/
|
2017-01-18 21:29:11 +03:00
|
|
|
toPlaintext() {
|
2017-02-02 14:45:21 +03:00
|
|
|
const renderer = new commonmark.HtmlRenderer({safe: false});
|
|
|
|
const real_paragraph = renderer.paragraph;
|
2017-01-18 21:29:11 +03:00
|
|
|
|
|
|
|
// The default `out` function only sends the input through an XML
|
|
|
|
// escaping function, which causes messages to be entity encoded,
|
|
|
|
// which we don't want in this case.
|
2017-02-02 14:45:21 +03:00
|
|
|
renderer.out = function(s) {
|
2017-01-18 21:29:11 +03:00
|
|
|
// The `lit` function adds a string literal to the output buffer.
|
|
|
|
this.lit(s);
|
2017-01-20 17:22:27 +03:00
|
|
|
};
|
2017-01-18 21:29:11 +03:00
|
|
|
|
2017-02-02 14:45:21 +03:00
|
|
|
renderer.paragraph = function(node, entering) {
|
2017-02-02 17:17:07 +03:00
|
|
|
// as with toHTML, only append lines to paragraphs if there are
|
|
|
|
// multiple paragraphs
|
|
|
|
if (is_multi_line(node)) {
|
|
|
|
if (!entering && node.next) {
|
2017-01-18 21:29:11 +03:00
|
|
|
this.lit('\n\n');
|
2016-12-02 21:58:35 +03:00
|
|
|
}
|
2016-09-22 20:57:46 +03:00
|
|
|
}
|
2017-01-20 17:22:27 +03:00
|
|
|
};
|
2017-02-02 17:17:07 +03:00
|
|
|
renderer.html_block = function(node) {
|
|
|
|
this.lit(node.literal);
|
|
|
|
if (is_multi_line(node) && node.next) this.lit('\n\n');
|
2017-10-11 19:56:17 +03:00
|
|
|
};
|
2016-09-22 20:57:46 +03:00
|
|
|
|
2017-02-02 14:45:21 +03:00
|
|
|
return renderer.render(this.parsed);
|
2016-09-22 19:18:12 +03:00
|
|
|
}
|
|
|
|
}
|