From 5373007301986335e89bc1262bea86e86bf36671 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 21 May 2019 17:34:18 +0200 Subject: [PATCH 1/4] initial attempt at converting html back to markdown --- src/editor/deserialize.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/editor/deserialize.js b/src/editor/deserialize.js index d440f9d336..71fe71d68a 100644 --- a/src/editor/deserialize.js +++ b/src/editor/deserialize.js @@ -40,11 +40,25 @@ function parseHtmlMessage(html, room) { switch (prefix) { case "@": return new UserPillPart(resourceId, n.textContent, room.getMember(resourceId)); case "#": return new RoomPillPart(resourceId); - default: return new PlainPart(n.textContent); + default: { + if (href === n.textContent) { + return new PlainPart(n.textContent); + } else { + return new PlainPart(`[${n.textContent}](${href})`); + } + } } } case "BR": return new NewlinePart("\n"); + case "EM": + return new PlainPart(`*${n.textContent}*`); + case "STRONG": + return new PlainPart(`**${n.textContent}**`); + case "PRE": + return new PlainPart(`\`\`\`${n.textContent}\`\`\``); + case "CODE": + return new PlainPart(`\`${n.textContent}\``); default: return new PlainPart(n.textContent); } From 53b6586986ccbbe8e8390091f0c3c54061ab76fd Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 21 May 2019 17:59:54 +0200 Subject: [PATCH 2/4] re-apply markdown when saving a message --- src/components/views/elements/MessageEditor.js | 7 ++++--- src/editor/serialize.js | 16 +++++++++++++--- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/components/views/elements/MessageEditor.js b/src/components/views/elements/MessageEditor.js index b07eca22b6..cb5767b192 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/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) { From 5f5a2f71402c1c1bcf2e4772e2c56b1d3a2d1432 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 21 May 2019 19:45:12 +0200 Subject: [PATCH 3/4] put code block on new line --- src/editor/deserialize.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/editor/deserialize.js b/src/editor/deserialize.js index 71fe71d68a..8a410ea105 100644 --- a/src/editor/deserialize.js +++ b/src/editor/deserialize.js @@ -56,7 +56,7 @@ function parseHtmlMessage(html, room) { case "STRONG": return new PlainPart(`**${n.textContent}**`); case "PRE": - return new PlainPart(`\`\`\`${n.textContent}\`\`\``); + return new PlainPart(`\`\`\`\n${n.textContent}\`\`\``); case "CODE": return new PlainPart(`\`${n.textContent}\``); default: From 723086e4d768aa2d17f69cde06bede41c1128862 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 22 May 2019 13:00:39 +0200 Subject: [PATCH 4/4] Decend into P & DIV elements while parsing a message. Also split on newline so all newlines are represented by a newlinepart --- src/editor/deserialize.js | 119 ++++++++++++++++++++++++++++---------- 1 file changed, 88 insertions(+), 31 deletions(-) diff --git a/src/editor/deserialize.js b/src/editor/deserialize.js index 8a410ea105..b3f4fe5b80 100644 --- a/src/editor/deserialize.js +++ b/src/editor/deserialize.js @@ -18,54 +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: { - if (href === n.textContent) { - return new PlainPart(n.textContent); - } else { - return new PlainPart(`[${n.textContent}](${href})`); - } - } + 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": - return new PlainPart(`*${n.textContent}*`); + parts.push(new PlainPart(`*${n.textContent}*`)); + break; case "STRONG": - return new PlainPart(`**${n.textContent}**`); - case "PRE": - return new PlainPart(`\`\`\`\n${n.textContent}\`\`\``); + 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": - return new PlainPart(`\`${n.textContent}\``); + 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; }