/* Copyright 2024 New Vector Ltd. Copyright 2019 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ 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: Part | undefined = 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!" }); }); it("spoiler", function () { const parts = normalize(parseEvent(textMessage("/spoiler broiler"), createPartCreator())); expect(parts.length).toBe(1); expect(parts[0]).toStrictEqual({ type: "plain", text: "/spoiler broiler" }); }); }); 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("user pill with displayname containing linebreak", function () { const html = 'Hi Alice127.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
it.skip("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 = "> \\