diff --git a/src/editor/deserialize.js b/src/editor/deserialize.js
index f22647c695..64b219c2a9 100644
--- a/src/editor/deserialize.js
+++ b/src/editor/deserialize.js
@@ -17,32 +17,125 @@ limitations under the License.
import { MATRIXTO_URL_PATTERN } from '../linkify-matrix';
import { PlainPart, UserPillPart, RoomPillPart, NewlinePart } from "./parts";
+import { walkDOMDepthFirst } from "./dom";
const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN);
-function parseLink(a, parts, room) {
+function parseLink(a, 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(
+ return new UserPillPart(
resourceId,
a.textContent,
room.getMember(resourceId),
- ));
- break;
+ );
case "#":
- parts.push(new RoomPillPart(resourceId));
- break;
+ return new RoomPillPart(resourceId);
default: {
if (href === a.textContent) {
- parts.push(new PlainPart(a.textContent));
+ return new PlainPart(a.textContent);
} else {
- parts.push(new PlainPart(`[${a.textContent}](${href})`));
+ return new PlainPart(`[${a.textContent}](${href})`);
}
- break;
+ }
+ }
+}
+
+function parseCodeBlock(n) {
+ const parts = [];
+ 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"));
+ }
+ });
+ return parts;
+}
+
+function parseElement(n, room) {
+ switch (n.nodeName) {
+ case "A":
+ return parseLink(n, room);
+ case "BR":
+ return new NewlinePart("\n");
+ case "EM":
+ return new PlainPart(`*${n.textContent}*`);
+ case "STRONG":
+ return new PlainPart(`**${n.textContent}**`);
+ case "PRE":
+ return parseCodeBlock(n);
+ case "CODE":
+ return new PlainPart(`\`${n.textContent}\``);
+ case "DEL":
+ return new PlainPart(`${n.textContent}`);
+ case "LI":
+ if (n.parentElement.nodeName === "OL") {
+ return new PlainPart(` 1. `);
+ } else {
+ return new PlainPart(` - `);
+ }
+ default:
+ // don't textify block nodes we'll decend into
+ if (!checkDecendInto(n)) {
+ return new PlainPart(n.textContent);
+ }
+ }
+}
+
+function checkDecendInto(node) {
+ switch (node.nodeName) {
+ case "PRE":
+ // a code block is textified in parseCodeBlock
+ // as we don't want to preserve markup in it,
+ // so no need to decend into it
+ return false;
+ default:
+ return checkBlockNode(node);
+ }
+}
+
+function checkBlockNode(node) {
+ switch (node.nodeName) {
+ case "PRE":
+ case "BLOCKQUOTE":
+ case "DIV":
+ case "P":
+ case "UL":
+ case "OL":
+ case "LI":
+ return true;
+ default:
+ return false;
+ }
+}
+
+function checkIgnored(n) {
+ if (n.nodeType === Node.TEXT_NODE) {
+ // riot adds \n text nodes in a lot of places,
+ // which should be ignored
+ return n.nodeValue === "\n";
+ } else if (n.nodeType === Node.ELEMENT_NODE) {
+ return n.nodeName === "MX-REPLY";
+ }
+ return true;
+}
+
+function prefixQuoteLines(isFirstNode, parts) {
+ const PREFIX = "> ";
+ // a newline (to append a > to) wouldn't be added to parts for the first line
+ // if there was no content before the BLOCKQUOTE, so handle that
+ if (isFirstNode) {
+ parts.splice(0, 0, new PlainPart(PREFIX));
+ }
+ for (let i = 0; i < parts.length; i += 1) {
+ if (parts[i].type === "newline") {
+ parts.splice(i + 1, 0, new PlainPart(PREFIX));
+ i += 1;
}
}
}
@@ -51,83 +144,64 @@ function parseHtmlMessage(html, room) {
// 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 root = new DOMParser().parseFromString(html, "text/html").body;
- let n = root.firstChild;
+ const rootNode = new DOMParser().parseFromString(html, "text/html").body;
const parts = [];
- let isFirstNode = true;
- while (n && n !== root) {
- switch (n.nodeType) {
- case Node.TEXT_NODE:
- // 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":
- break;
- 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":
- 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;
- case "DEL":
- parts.push(new PlainPart(`${n.textContent}`));
- break;
- default:
- parts.push(new PlainPart(n.textContent));
- break;
- }
- break;
+ let lastNode;
+ let inQuote = false;
+
+ function onNodeEnter(n) {
+ if (checkIgnored(n)) {
+ return false;
}
- // go up if we can't go next
- if (!n.nextSibling) {
- n = n.parentElement;
+ if (n.nodeName === "BLOCKQUOTE") {
+ inQuote = true;
}
- n = n.nextSibling;
- isFirstNode = false;
+
+ const newParts = [];
+ if (lastNode && (checkBlockNode(lastNode) || checkBlockNode(n))) {
+ newParts.push(new NewlinePart("\n"));
+ }
+
+ if (n.nodeType === Node.TEXT_NODE) {
+ newParts.push(new PlainPart(n.nodeValue));
+ } else if (n.nodeType === Node.ELEMENT_NODE) {
+ const parseResult = parseElement(n, room);
+ if (parseResult) {
+ if (Array.isArray(parseResult)) {
+ newParts.push(...parseResult);
+ } else {
+ newParts.push(parseResult);
+ }
+ }
+ }
+
+ if (newParts.length && inQuote) {
+ const isFirstPart = parts.length === 0;
+ prefixQuoteLines(isFirstPart, newParts);
+ }
+
+ parts.push(...newParts);
+
+ // extra newline after quote, only if there something behind it...
+ if (lastNode && lastNode.nodeName === "BLOCKQUOTE") {
+ parts.push(new NewlinePart("\n"));
+ }
+ lastNode = null;
+ return checkDecendInto(n);
}
+
+ function onNodeLeave(n) {
+ if (checkIgnored(n)) {
+ return;
+ }
+ if (n.nodeName === "BLOCKQUOTE") {
+ inQuote = false;
+ }
+ lastNode = n;
+ }
+
+ walkDOMDepthFirst(rootNode, onNodeEnter, onNodeLeave);
+
return parts;
}
diff --git a/src/editor/dom.js b/src/editor/dom.js
index ffdb8bed68..3ef1df24c3 100644
--- a/src/editor/dom.js
+++ b/src/editor/dom.js
@@ -15,22 +15,22 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-function walkDOMDepthFirst(editor, enterNodeCallback, leaveNodeCallback) {
- let node = editor.firstChild;
- while (node && node !== editor) {
- enterNodeCallback(node);
- if (node.firstChild) {
+export function walkDOMDepthFirst(rootNode, enterNodeCallback, leaveNodeCallback) {
+ let node = rootNode.firstChild;
+ while (node && node !== rootNode) {
+ const shouldDecend = enterNodeCallback(node);
+ if (shouldDecend && node.firstChild) {
node = node.firstChild;
} else if (node.nextSibling) {
node = node.nextSibling;
} else {
- while (!node.nextSibling && node !== editor) {
+ while (!node.nextSibling && node !== rootNode) {
node = node.parentElement;
- if (node !== editor) {
+ if (node !== rootNode) {
leaveNodeCallback(node);
}
}
- if (node !== editor) {
+ if (node !== rootNode) {
node = node.nextSibling;
}
}
@@ -62,6 +62,7 @@ export function getCaretOffsetAndText(editor, sel) {
}
text += nodeText;
}
+ return true;
}
function leaveNodeCallback(node) {