/* Copyright 2019 The Matrix.org Foundation C.I.C. 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. */ import { MatrixEvent } from "matrix-js-sdk/src/matrix"; import { parseEvent } from "../../src/editor/deserialize"; import { Part } from "../../src/editor/parts"; import { createPartCreator } from "./mock"; const FOUR_SPACES = " ".repeat(4); function htmlMessage(formattedBody: string, msgtype = "m.text") { return { getContent() { return { msgtype, format: "org.matrix.custom.html", formatted_body: formattedBody, }; }, } as unknown as MatrixEvent; } function textMessage(body: string, msgtype = "m.text") { return { getContent() { return { msgtype, body, }; }, } as unknown as MatrixEvent; } function textMessageReply(body: string, msgtype = "m.text") { return { ...textMessage(body, msgtype), replyEventId: "!foo:bar", } as unknown as MatrixEvent; } function mergeAdjacentParts(parts: Part[]) { let prevPart: Part | undefined; for (let i = 0; i < parts.length; ++i) { let part = parts[i]; const isEmpty = !part.text.length; const isMerged = !isEmpty && prevPart && prevPart.merge?.(part); if (isEmpty || isMerged) { // remove empty or merged part part = prevPart; parts.splice(i, 1); //repeat this index, as it's removed now --i; } prevPart = part; } } function normalize(parts: Part[]) { // merge adjacent parts as this will happen // in the model anyway, and whether 1 or multiple // plain parts are returned is an implementation detail mergeAdjacentParts(parts); // convert to data objects for easier asserting return parts.map((p) => p.serialize()); } describe("editor/deserialize", function () { describe("text messages", function () { it("test with newlines", function () { const parts = normalize(parseEvent(textMessage("hello\nworld"), createPartCreator())); expect(parts[0]).toStrictEqual({ type: "plain", text: "hello" }); expect(parts[1]).toStrictEqual({ type: "newline", text: "\n" }); expect(parts[2]).toStrictEqual({ type: "plain", text: "world" }); expect(parts.length).toBe(3); }); it("@room pill", function () { const parts = normalize(parseEvent(textMessage("text message for @room"), createPartCreator())); expect(parts.length).toBe(2); expect(parts[0]).toStrictEqual({ type: "plain", text: "text message for " }); expect(parts[1]).toStrictEqual({ type: "at-room-pill", text: "@room" }); }); it("emote", function () { const text = "says DON'T SHOUT!"; const parts = normalize(parseEvent(textMessage(text, "m.emote"), createPartCreator())); expect(parts.length).toBe(1); expect(parts[0]).toStrictEqual({ type: "plain", text: "/me says DON'T SHOUT!" }); }); }); describe("html messages", function () { it("inline styling", function () { const html = "bold and emphasized text"; const parts = normalize(parseEvent(htmlMessage(html), createPartCreator())); expect(parts.length).toBe(1); expect(parts[0]).toStrictEqual({ type: "plain", text: "**bold** and _emphasized_ text" }); }); it("hyperlink", function () { const html = 'click this!'; const parts = normalize(parseEvent(htmlMessage(html), createPartCreator())); expect(parts.length).toBe(1); expect(parts[0]).toStrictEqual({ type: "plain", text: "click [this](http://example.com/)!" }); }); it("multiple lines with paragraphs", function () { const html = "
hello
world
"; const parts = normalize(parseEvent(htmlMessage(html), createPartCreator())); expect(parts.length).toBe(4); expect(parts[0]).toStrictEqual({ type: "plain", text: "hello" }); expect(parts[1]).toStrictEqual({ type: "newline", text: "\n" }); expect(parts[2]).toStrictEqual({ type: "newline", text: "\n" }); expect(parts[3]).toStrictEqual({ type: "plain", text: "world" }); }); it("multiple lines with line breaks", function () { const html = "hellohello
warm
world
"; const parts = normalize(parseEvent(htmlMessage(html), createPartCreator())); expect(parts.length).toBe(6); expect(parts[0]).toStrictEqual({ type: "plain", text: "hello" }); expect(parts[1]).toStrictEqual({ type: "newline", text: "\n" }); expect(parts[2]).toStrictEqual({ type: "plain", text: "warm" }); expect(parts[3]).toStrictEqual({ type: "newline", text: "\n" }); expect(parts[4]).toStrictEqual({ type: "newline", text: "\n" }); expect(parts[5]).toStrictEqual({ type: "plain", text: "world" }); }); it("quote", function () { const html = "wise
words
indeed
"; const parts = normalize(parseEvent(htmlMessage(html), createPartCreator())); expect(parts.length).toBe(6); expect(parts[0]).toStrictEqual({ type: "plain", text: "> _wise_" }); expect(parts[1]).toStrictEqual({ type: "newline", text: "\n" }); expect(parts[2]).toStrictEqual({ type: "plain", text: "> **words**" }); expect(parts[3]).toStrictEqual({ type: "newline", text: "\n" }); expect(parts[4]).toStrictEqual({ type: "newline", text: "\n" }); expect(parts[5]).toStrictEqual({ type: "plain", text: "indeed" }); }); it("user pill", function () { const html = 'Hi Alice!'; const parts = normalize(parseEvent(htmlMessage(html), createPartCreator())); expect(parts.length).toBe(3); expect(parts[0]).toStrictEqual({ type: "plain", text: "Hi " }); expect(parts[1]).toStrictEqual({ type: "user-pill", text: "Alice", resourceId: "@alice:hs.tld" }); expect(parts[2]).toStrictEqual({ type: "plain", text: "!" }); }); it("user pill with displayname containing backslash", function () { const html = 'Hi Alice\\!'; const parts = normalize(parseEvent(htmlMessage(html), createPartCreator())); expect(parts.length).toBe(3); expect(parts[0]).toStrictEqual({ type: "plain", text: "Hi " }); expect(parts[1]).toStrictEqual({ type: "user-pill", text: "Alice\\", resourceId: "@alice:hs.tld" }); expect(parts[2]).toStrictEqual({ type: "plain", text: "!" }); }); it("user pill with displayname containing opening square bracket", function () { const html = 'Hi Alice[[!'; const parts = normalize(parseEvent(htmlMessage(html), createPartCreator())); expect(parts.length).toBe(3); expect(parts[0]).toStrictEqual({ type: "plain", text: "Hi " }); expect(parts[1]).toStrictEqual({ type: "user-pill", text: "Alice[[", resourceId: "@alice:hs.tld" }); expect(parts[2]).toStrictEqual({ type: "plain", text: "!" }); }); it("user pill with displayname containing closing square bracket", function () { const html = 'Hi Alice]!'; const parts = normalize(parseEvent(htmlMessage(html), createPartCreator())); expect(parts.length).toBe(3); expect(parts[0]).toStrictEqual({ type: "plain", text: "Hi " }); expect(parts[1]).toStrictEqual({ type: "user-pill", text: "Alice]", resourceId: "@alice:hs.tld" }); expect(parts[2]).toStrictEqual({ type: "plain", text: "!" }); }); it("room pill", function () { const html = 'Try #room:hs.tld?'; const parts = normalize(parseEvent(htmlMessage(html), createPartCreator())); expect(parts.length).toBe(3); expect(parts[0]).toStrictEqual({ type: "plain", text: "Try " }); expect(parts[1]).toStrictEqual({ type: "room-pill", text: "#room:hs.tld", resourceId: "#room:hs.tld" }); expect(parts[2]).toStrictEqual({ type: "plain", text: "?" }); }); it("@room pill", function () { const html = "formatted message for @room"; const parts = normalize(parseEvent(htmlMessage(html), createPartCreator())); expect(parts.length).toBe(2); expect(parts[0]).toStrictEqual({ type: "plain", text: "_formatted_ message for " }); expect(parts[1]).toStrictEqual({ type: "at-room-pill", text: "@room" }); }); it("inline code", function () { const html = "there is no place like127.0.0.1
!";
const parts = normalize(parseEvent(htmlMessage(html), createPartCreator()));
expect(parts.length).toBe(1);
expect(parts[0]).toStrictEqual({ type: "plain", text: "there is no place like `127.0.0.1`!" });
});
it("code block with no trailing text", function () {
const html = "0xDEADBEEF\n
\n";
const parts = normalize(parseEvent(htmlMessage(html), createPartCreator()));
expect(parts.length).toBe(5);
expect(parts[0]).toStrictEqual({ type: "plain", text: "```" });
expect(parts[1]).toStrictEqual({ type: "newline", text: "\n" });
expect(parts[2]).toStrictEqual({ type: "plain", text: "0xDEADBEEF" });
expect(parts[3]).toStrictEqual({ type: "newline", text: "\n" });
expect(parts[4]).toStrictEqual({ type: "plain", text: "```" });
});
// failing likely because of https://github.com/vector-im/element-web/issues/10316
xit("code block with no trailing text and no newlines", function () {
const html = "0xDEADBEEF
";
const parts = normalize(parseEvent(htmlMessage(html), createPartCreator()));
expect(parts.length).toBe(5);
expect(parts[0]).toStrictEqual({ type: "plain", text: "```" });
expect(parts[1]).toStrictEqual({ type: "newline", text: "\n" });
expect(parts[2]).toStrictEqual({ type: "plain", text: "0xDEADBEEF" });
expect(parts[3]).toStrictEqual({ type: "newline", text: "\n" });
expect(parts[4]).toStrictEqual({ type: "plain", text: "```" });
});
it("unordered lists", function () {
const html = "foo"; const parts = normalize(parseEvent(htmlMessage(html), createPartCreator())); expect(parts).toMatchSnapshot(); }); it("surrounds lists with newlines", () => { const html = "foobar
this → ` is a backtick
and here are 3 of them:\n```
";
const parts = normalize(parseEvent(htmlMessage(html), createPartCreator()));
expect(parts).toMatchSnapshot();
});
it("escapes backticks outside of code blocks", () => {
const html = "some `backticks`";
const parts = normalize(parseEvent(htmlMessage(html), createPartCreator()));
expect(parts).toMatchSnapshot();
});
it("escapes backslashes", () => {
const html = "C:\\My Documents";
const parts = normalize(parseEvent(htmlMessage(html), createPartCreator()));
expect(parts).toMatchSnapshot();
});
it("escapes asterisks", () => {
const html = "*hello*";
const parts = normalize(parseEvent(htmlMessage(html), createPartCreator()));
expect(parts).toMatchSnapshot();
});
it("escapes underscores", () => {
const html = "__emphasis__";
const parts = normalize(parseEvent(htmlMessage(html), createPartCreator()));
expect(parts).toMatchSnapshot();
});
it("escapes square brackets", () => {
const html = "[not an actual link](https://example.org)";
const parts = normalize(parseEvent(htmlMessage(html), createPartCreator()));
expect(parts).toMatchSnapshot();
});
it("escapes angle brackets", () => {
const html = "> \\