diff --git a/src/components/views/elements/MessageEditor.js b/src/components/views/elements/MessageEditor.js
index 38d3f80a28..3478ce30a8 100644
--- a/src/components/views/elements/MessageEditor.js
+++ b/src/components/views/elements/MessageEditor.js
@@ -22,7 +22,7 @@ import dis from '../../../dispatcher';
import EditorModel from '../../../editor/model';
import {setCaretPosition} from '../../../editor/caret';
import {getCaretOffsetAndText} from '../../../editor/dom';
-import {htmlSerialize, textSerialize, requiresHtml} from '../../../editor/serialize';
+import {htmlSerializeIfNeeded, textSerialize} from '../../../editor/serialize';
import {parseEvent} from '../../../editor/deserialize';
import Autocomplete from '../rooms/Autocomplete';
import {PartCreator} from '../../../editor/parts';
@@ -128,9 +128,10 @@ export default class MessageEditor extends React.Component {
msgtype: newContent.msgtype,
body: ` * ${newContent.body}`,
};
- if (requiresHtml(this.model)) {
+ const formattedBody = htmlSerializeIfNeeded(this.model);
+ if (formattedBody) {
newContent.format = "org.matrix.custom.html";
- newContent.formatted_body = htmlSerialize(this.model);
+ newContent.formatted_body = formattedBody;
contentBody.format = newContent.format;
contentBody.formatted_body = ` * ${newContent.formatted_body}`;
}
diff --git a/src/editor/deserialize.js b/src/editor/deserialize.js
index d440f9d336..b3f4fe5b80 100644
--- a/src/editor/deserialize.js
+++ b/src/editor/deserialize.js
@@ -18,40 +18,111 @@ limitations under the License.
import { MATRIXTO_URL_PATTERN } from '../linkify-matrix';
import { PlainPart, UserPillPart, RoomPillPart, NewlinePart } from "./parts";
+const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN);
+
+function parseLink(a, parts, room) {
+ const {href} = a;
+ const pillMatch = REGEX_MATRIXTO.exec(href) || [];
+ const resourceId = pillMatch[1]; // The room/user ID
+ const prefix = pillMatch[2]; // The first character of prefix
+ switch (prefix) {
+ case "@":
+ parts.push(new UserPillPart(
+ resourceId,
+ a.textContent,
+ room.getMember(resourceId),
+ ));
+ break;
+ case "#":
+ parts.push(new RoomPillPart(resourceId));
+ break;
+ default: {
+ if (href === a.textContent) {
+ parts.push(new PlainPart(a.textContent));
+ } else {
+ parts.push(new PlainPart(`[${a.textContent}](${href})`));
+ }
+ break;
+ }
+ }
+}
+
function parseHtmlMessage(html, room) {
- const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN);
// no nodes from parsing here should be inserted in the document,
// as scripts in event handlers, etc would be executed then.
// we're only taking text, so that is fine
- const nodes = Array.from(new DOMParser().parseFromString(html, "text/html").body.childNodes);
- const parts = nodes.map(n => {
+ const root = new DOMParser().parseFromString(html, "text/html").body;
+ let n = root.firstChild;
+ const parts = [];
+ let isFirstNode = true;
+ while (n && n !== root) {
switch (n.nodeType) {
case Node.TEXT_NODE:
- return new PlainPart(n.nodeValue);
+ // the plainpart doesn't accept \n and will cause
+ // a newlinepart to be created.
+ if (n.nodeValue !== "\n") {
+ parts.push(new PlainPart(n.nodeValue));
+ }
+ break;
case Node.ELEMENT_NODE:
switch (n.nodeName) {
- case "MX-REPLY":
- return null;
- case "A": {
- const {href} = n;
- const pillMatch = REGEX_MATRIXTO.exec(href) || [];
- const resourceId = pillMatch[1]; // The room/user ID
- const prefix = pillMatch[2]; // The first character of prefix
- switch (prefix) {
- case "@": return new UserPillPart(resourceId, n.textContent, room.getMember(resourceId));
- case "#": return new RoomPillPart(resourceId);
- default: return new PlainPart(n.textContent);
+ case "DIV":
+ case "P": {
+ // block element should cause line break if not first
+ if (!isFirstNode) {
+ parts.push(new NewlinePart("\n"));
+ }
+ // decend into paragraph or div
+ if (n.firstChild) {
+ n = n.firstChild;
+ continue;
+ } else {
+ break;
}
}
+ case "A": {
+ parseLink(n, parts, room);
+ break;
+ }
case "BR":
- return new NewlinePart("\n");
+ parts.push(new NewlinePart("\n"));
+ break;
+ case "EM":
+ parts.push(new PlainPart(`*${n.textContent}*`));
+ break;
+ case "STRONG":
+ parts.push(new PlainPart(`**${n.textContent}**`));
+ break;
+ case "PRE": {
+ // block element should cause line break if not first
+ if (!isFirstNode) {
+ parts.push(new NewlinePart("\n"));
+ }
+ const preLines = `\`\`\`\n${n.textContent}\`\`\``.split("\n");
+ preLines.forEach((l, i) => {
+ parts.push(new PlainPart(l));
+ if (i < preLines.length - 1) {
+ parts.push(new NewlinePart("\n"));
+ }
+ });
+ break;
+ }
+ case "CODE":
+ parts.push(new PlainPart(`\`${n.textContent}\``));
+ break;
default:
- return new PlainPart(n.textContent);
+ parts.push(new PlainPart(n.textContent));
+ break;
}
- default:
- return null;
+ break;
}
- }).filter(p => !!p);
+ // go up if we can't go next
+ if (!n.nextSibling) {
+ n = n.parentElement;
+ }
+ n = n.nextSibling;
+ isFirstNode = false;
+ }
return parts;
}
diff --git a/src/editor/serialize.js b/src/editor/serialize.js
index 1724e4a2b7..73fbbe5d01 100644
--- a/src/editor/serialize.js
+++ b/src/editor/serialize.js
@@ -15,21 +15,31 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-export function htmlSerialize(model) {
+import Markdown from '../Markdown';
+
+export function mdSerialize(model) {
return model.parts.reduce((html, part) => {
switch (part.type) {
case "newline":
- return html + "
";
+ return html + "\n";
case "plain":
case "pill-candidate":
return html + part.text;
case "room-pill":
case "user-pill":
- return html + `${part.text}`;
+ return html + `[${part.text}](https://matrix.to/#/${part.resourceId})`;
}
}, "");
}
+export function htmlSerializeIfNeeded(model) {
+ const md = mdSerialize(model);
+ const parser = new Markdown(md);
+ if (!parser.isPlainText()) {
+ return parser.toHTML();
+ }
+}
+
export function textSerialize(model) {
return model.parts.reduce((text, part) => {
switch (part.type) {