/* Copyright 2024 New Vector Ltd. Copyright 2022 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 EditorModel from "../../src/editor/model"; import DocumentOffset from "../../src/editor/offset"; import { htmlSerializeIfNeeded, textSerialize } from "../../src/editor/serialize"; import { createPartCreator } from "./mock"; function htmlMessage(formattedBody: string, msgtype = "m.text") { return { getContent() { return { msgtype, format: "org.matrix.custom.html", formatted_body: formattedBody, }; }, } as unknown as MatrixEvent; } async function md2html(markdown: string): Promise { const pc = createPartCreator(); const oldModel = new EditorModel([], pc, () => {}); await oldModel.update(markdown, "insertText", new DocumentOffset(markdown.length, false)); return htmlSerializeIfNeeded(oldModel, { forceHTML: true })!; } function html2md(html: string): string { const pc = createPartCreator(); const parts = parseEvent(htmlMessage(html), pc); const newModel = new EditorModel(parts, pc); return textSerialize(newModel); } async function roundTripMarkdown(markdown: string): Promise { return html2md(await md2html(markdown)); } async function roundTripHtml(html: string): Promise { return await md2html(html2md(html)); } describe("editor/roundtrip", function () { describe("markdown messages should round-trip if they contain", function () { test.each([ ["newlines", "hello\nworld"], ["pills", "text message for @room"], ["pills with interesting characters in mxid", "text message for @alice\\\\\\_\\]#>&:hs.example.com"], ["styling", "**bold** and _emphasised_"], ["bold within a word", "abso**fragging**lutely"], ["escaped html", "a\\b"], ["escaped markdown", "\\*\\*foo\\*\\* \\_bar\\_ \\[a\\](b)"], ["escaped backslashes", "C:\\\\Program Files"], ["code in backticks", "foo ->`x`"], ["code blocks containing backticks", "```\nfoo ->`x`\nbar\n```"], ["code blocks containing markdown", "```\n__init__.py\n```"], ["nested formatting", "ab **c _d_ e** fg"], ["an ordered list", "A\n\n1. b\n2. c\n3. d\nE"], ["an ordered list starting later", "A\n\n9. b\n10. c\n11. d\nE"], ["an unordered list", "A\n\n- b\n- c\n- d\nE"], ["code block followed by text after a blank line", "```A\nfoo(bar).baz();\n\n3\n```\n\nB"], ["just a code block", "```\nfoo(bar).baz();\n\n3\n```"], ["code block with language specifier", "```bash\nmake install\n\n```"], ["inline code", "there's no place `127.0.0.1` like"], ["nested quotations", "saying\n\n> > foo\n\n> NO\n\nis valid"], ["quotations", "saying\n\n> NO\n\nis valid"], ["links", "click [this](http://example.com/)!"], ])("%s", async (_name, markdown) => { expect(await roundTripMarkdown(markdown)).toEqual(markdown); }); test.skip.each([ // Removes trailing spaces ["a code block followed by newlines", "```\nfoo(bar).baz();\n\n3\n```\n\n"], // Adds a space after the code block ["a code block surrounded by text", "```A\nfoo(bar).baz();\n\n3\n```\nB"], // Adds a space before the list ["an unordered list directly preceded by text", "A\n- b\n- c\n- d\nE"], // Re-numbers to 1, 2, 3 ["an ordered list where everything is 1", "A\n\n1. b\n1. c\n1. d\nE"], // Adds a space before the list ["an ordered list directly preceded by text", "A\n1. b\n2. c\n3. d\nE"], // Adds and removes spaces before the nested list ["nested unordered lists", "A\n- b\n- c\n - c1\n - c2\n- d\nE"], // Adds and removes spaces before the nested list ["nested ordered lists", "A\n\n1. b\n2. c\n 1. c1\n 2. c2\n3. d\nE"], // Adds and removes spaces before the nested list ["nested mixed lists", "A\n\n1. b\n2. c\n - c1\n - c2\n3. d\nE"], // Backslashes get doubled ["backslashes", "C:\\Program Files"], // Deletes the whitespace ["newlines with trailing and leading whitespace", "hello \n world"], // Escapes the underscores ["underscores within a word", "abso_fragging_lutely"], // Includes the trailing text into the quotation // https://github.com/vector-im/element-web/issues/22341 ["quotations without separating newlines", "saying\n> NO\nis valid"], // Removes trailing and leading whitespace ["quotations with trailing and leading whitespace", "saying \n\n> NO\n\n is valid"], ])("%s", async (_name, markdown) => { expect(await roundTripMarkdown(markdown)).toEqual(markdown); }); it("styling, but * becomes _ and __ becomes **", async function () { expect(await roundTripMarkdown("__bold__ and *emphasised*")).toEqual("**bold** and _emphasised_"); }); }); describe("HTML messages should round-trip if they contain", function () { test.each([ ["backslashes", "C:\\Program Files"], [ "nested blockquotes", "
\n

foo

\n
\n

bar

\n
\n
\n", ], ["ordered lists", "
    \n
  1. asd
  2. \n
  3. fgd
  4. \n
\n"], ["ordered lists starting later", '
    \n
  1. asd
  2. \n
  3. fgd
  4. \n
\n'], ["unordered lists", "
    \n
  • asd
  • \n
  • fgd
  • \n
\n"], ["code blocks with surrounding text", "

a

\n
a\ny;\n
\n

b

\n"], ["code blocks", "
a\ny;\n
\n"], ["code blocks containing markdown", "
__init__.py\n
\n"], ["code blocks with language specifier", '
__init__.py\n
\n'], ["paragraphs including formatting", "

one

\n

t w o

\n"], ["paragraphs", "

one

\n

two

\n"], ["links", "http://more.example.com/"], ["escaped html", "This >em<isn't>em< important"], ["markdown-like symbols", "You _would_ **type** [a](http://this.example.com) this."], ["formatting within a word", "absofragginglutely"], ["formatting", "This is important"], ["line breaks", "one
two"], ])("%s", async (_name, html) => { expect(await roundTripHtml(html)).toEqual(html); }); test.skip.each([ // Strips out the pill - maybe needs some user lookup to work? ["user pills", 'Alice'], // Appends a slash to the URL // https://github.com/vector-im/element-web/issues/22342 ["links without trailing slashes", 'Go here to see more'], // Inserts newlines after tags ["paragraphs without newlines", "

one

two

"], // Inserts a code block ["nested lists", "
    \n
  1. asd
  2. \n
  3. \n
      \n
    • fgd
    • \n
    • sdf
    • \n
    \n
  4. \n
\n"], ])("%s", async (_name, html) => { expect(await roundTripHtml(html)).toEqual(html); }); }); });