/
githubmirror
/
zulip
Обзор
Документация
Войти
/
githubmirror
/
zulip
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
web/tests/compose_paste.test.cjs
844 строки
80 KB
sandeep
compose paste: Convert mention HTML to markdown when quoting.
04 авг 2026, 13:09
04 авг 2026, 13:09
1f38613
Код
Авторство
О чём код?
"use strict"; const assert = require("node:assert/strict"); const {JSDOM} = require("jsdom"); const katex_tests = require("../../zerver/tests/fixtures/katex_test_cases.json"); const {parse} = require("../src/markdown.ts"); const {make_user_group} = require("./lib/example_group.cjs"); const {make_stream} = require("./lib/example_stream.cjs"); const {make_user} = require("./lib/example_user.cjs"); const {mock_esm, zrequire, set_global} = require("./lib/namespace.cjs"); const {run_test, noop} = require("./lib/test.cjs"); const {$} = require("./lib/zjquery.cjs"); const {window} = new JSDOM(); const text_field_edit = mock_esm("text-field-edit", {insertTextIntoField: noop}); const compose_paste = zrequire("compose_paste"); const compose_ui = zrequire("compose_ui"); const linkifiers = zrequire("linkifiers"); const markdown = zrequire("markdown"); const markdown_config = zrequire("markdown_config"); const people = zrequire("people"); const stream_data = zrequire("stream_data"); const user_groups = zrequire("user_groups"); const {initialize_user_settings} = zrequire("user_settings"); set_global("document", {}); class ClipboardEvent { constructor({clipboardData} = {}) { this.clipboardData = clipboardData; } } set_global("ClipboardEvent", ClipboardEvent); initialize_user_settings({ user_settings: { translate_emoticons: false, }, }); markdown.initialize(markdown_config.get_helpers()); // User and group ids match the ids used in the mention test fixtures // below. const king_hamlet = make_user({user_id: 10, full_name: "King Hamlet"}); const desdemona = make_user({user_id: 9, full_name: "Desdemona"}); const polonius = make_user({user_id: 13, full_name: "Polonius"}); people.add_active_user(king_hamlet); people.add_active_user(desdemona); people.add_active_user(polonius); const hamletcharacters = make_user_group({name: "hamletcharacters", id: 24}); const owners = make_user_group({name: "role:owners", id: 25, is_system_group: true}); user_groups.initialize({realm_user_groups: [hamletcharacters, owners]}); stream_data.add_sub_for_tests( make_stream({ stream_id: 4, name: "Rome", }), ); stream_data.add_sub_for_tests( make_stream({ stream_id: 5, name: "Romeo`s lair", }), ); run_test("try_stream_topic_syntax_text", () => { const test_cases = [ [ "http://zulip.zulipdev.com/#narrow/channel/4-Rome/topic/old.20FAILED.20EXPORT", "#**Rome>old FAILED EXPORT**", ], [ "http://zulip.zulipdev.com/#narrow/channel/4-Rome/topic/100.25.20profits", "#**Rome>100% profits**", ], [ "http://zulip.zulipdev.com/#narrow/channel/4-Rome/topic/old.20API.20wasn't.20compiling.20erratically", "#**Rome>old API wasn't compiling erratically**", ], ["http://different.origin.com/#narrow/channel/4-Rome/topic/old.20FAILED.20EXPORT"], [ "http://zulip.zulipdev.com/#narrow/channel/4-Rome/topic/old.20FAILED.20EXPORT/near/100", "#**Rome>old FAILED EXPORT@100**", ], ["http://zulip.zulipdev.com/#narrow/channel/4-Rome/topic//near/100", "#**Rome>@100**"], [ "http://zulip.zulipdev.com/#narrow/channel/4-Rome/topic/old.20FAILED.20EXPORT/with/100", "#**Rome>old FAILED EXPORT**", ], // malformed urls ["http://zulip.zulipdev.com/narrow/channel/4-Rome/topic/old.20FAILED.20EXPORT"], ["http://zulip.zulipdev.com/#not_narrow/channel/4-Rome/topic/old.20FAILED.20EXPORT"], ["http://zulip.zulipdev.com/#narrow/not_stream/4-Rome/topic/old.20FAILED.20EXPORT"], ["http://zulip.zulipdev.com/#narrow/channel/4-Rome/not_topic/old.20FAILED.20EXPORT"], ["http://zulip.zulipdev.com/#narrow/channel/4-Rome/", "#**Rome**"], ["http://zulip.zulipdev.com/#narrow/channel/4-Rome/topic"], ["http://zulip.zulipdev.com/#narrow/topic/cheese"], ["http://zulip.zulipdev.com/#narrow/topic/pizza/stream/Rome"], ["http://zulip.zulipdev.com/#narrow/channel/4-Rome/topic/old.20FAILED.20EXPORT/near/"], // When a url containing characters which are known to produce broken // #**stream>topic** urls is pasted, a normal markdown link syntax is produced. [ "http://zulip.zulipdev.com/#narrow/stream/4-Rome/topic/100.25.20profits.60", "[#Rome > 100% profits`](#narrow/channel/4-Rome/topic/100.25.20profits.60)", ], [ "http://zulip.zulipdev.com/#narrow/stream/4-Rome/topic/100.25.20*profits", "[#Rome > 100% *profits](#narrow/channel/4-Rome/topic/100.25.20.2Aprofits)", ], [ "http://zulip.zulipdev.com/#narrow/stream/4-Rome/topic/.24.24 100.25.20profits", "[#Rome > $$ 100% profits](#narrow/channel/4-Rome/topic/.24.24.20100.25.20profits)", ], [ "http://zulip.zulipdev.com/#narrow/stream/4-Rome/topic/>100.25.20profits", "[#Rome > >100% profits](#narrow/channel/4-Rome/topic/.3E100.25.20profits)", ], [ "http://zulip.zulipdev.com/#narrow/stream/5-Romeo.60s-lair/topic/normal", "[#Romeo`s lair > normal](#narrow/channel/5-Romeo.60s-lair/topic/normal)", ], [ "http://zulip.zulipdev.com/#narrow/stream/4-Rome/topic/100.25.20profits.60/near/20", "[#Rome > 100% profits` @ 💬](#narrow/channel/4-Rome/topic/100.25.20profits.60/near/20)", ], ]; for (const test_case of test_cases) { const result = compose_paste.try_stream_topic_syntax_text(test_case[0]); const expected = test_case[1] ?? null; assert.equal(result, expected, "Failed for url: " + test_case[0]); } }); run_test("maybe_transform_html", () => { // Copied HTML from VS Code let paste_html = `<div style="color: #cccccc;background-color: #1f1f1f;font-family: 'Droid Sans Mono', 'monospace', monospace;font-weight: normal;font-size: 14px;line-height: 19px;white-space: pre;"><div><span style="color: #c586c0;">if</span><span style="color: #cccccc;"> (</span><span style="color: #9cdcfe;">$preview_src</span><span style="color: #cccccc;">.</span><span style="color: #dcdcaa;">endsWith</span><span style="color: #cccccc;">(</span><span style="color: #ce9178;">"&size=full"</span><span style="color: #cccccc;">))</span></div></div>`; let paste_text = `if ($preview_src.endsWith("&size=full"))`; const escaped_paste_text = "if ($preview_src.endsWith("&size=full"))"; const expected_output = "<pre><code>" + escaped_paste_text + "</code></pre>"; assert.equal(compose_paste.maybe_transform_html(paste_html, paste_text), expected_output); // Untransformed HTML paste_html = "<div><div>Hello</div><div>World!</div></div>"; paste_text = "Hello\nWorld!"; assert.equal(compose_paste.maybe_transform_html(paste_html, paste_text), paste_html); }); run_test("paste_handler reverse linkify", ({override, override_rewire}) => { global.document = window.document; global.window = window; global.Node = window.Node; global.HTMLElement = window.HTMLElement; global.HTMLAnchorElement = window.HTMLAnchorElement; global.HTMLTextAreaElement = window.HTMLTextAreaElement; linkifiers.update_linkifier_rules([ { id: 1, pattern: "#D(?P<id>[0-9]{2,8})", url_template: "https://github.com/zulip/zulip-desktop/pull/{id}", reverse_template: "#D{id}", alternative_url_templates: ["https://github.com/zulip/zulip-desktop/issues/{id}"], }, ]); let inserted_text; let undo_texts; override_rewire(compose_ui, "insert_and_scroll_into_view", (text) => { inserted_text = text; }); override(text_field_edit, "insertTextIntoField", (_textarea, text) => { undo_texts.push(text); }); const html_with_formatting = ` <p> <span class="katex"> <span class="katex-mathml"> <math xmlns="http://www.w3.org/1998/Math/MathML"> <semantics> <mrow> <mi>x</mi> <mo>+</mo> <mi>y</mi> <mn>2</mn> </mrow> <annotation encoding="application/x-tex">x + y2</annotation> </semantics> </math> </span> <span class="katex-html" aria-hidden="true"> <span class="base"> <span class="mord mathnormal">x</span> <span class="mbin">+</span> </span> <span class="base"> <span class="mord mathnormal">y</span> <span class="mord">2</span> </span> </span> </span> <del>test</del> <a href="https://github.com/zulip/zulip-desktop/pull/1359">https://github.com/zulip/zulip-desktop/pull/1359</a> </p>`; const test_cases = [ { // Reverse linkify should preserve formatting when pasting HTML. paste_html: html_with_formatting, paste_text: "x\n+\ny\n2\nx + y2\nx+y2 test https://github.com/zulip/zulip-desktop/pull/1359", expected: "$$x + y2$$ ~~test~~ #D1359", // When paste_html is a real value, there are two undo states: // first undo reverses the reverse linkify (back to formatted text), // second undo reverses the formatting (back to plain text). expected_undo_texts: [ "x\n+\ny\n2\nx + y2\nx+y2 test https://github.com/zulip/zulip-desktop/pull/1359", "$$x + y2$$ ~~test~~ https://github.com/zulip/zulip-desktop/pull/1359", ], }, { // Reverse linkify should work for URL only text. paste_html: "", paste_text: "https://github.com/zulip/zulip-desktop/pull/1359", expected: "#D1359", expected_undo_texts: ["https://github.com/zulip/zulip-desktop/pull/1359"], }, { // Reverse linkify should work in plain text with surrounding text. paste_html: "", paste_text: "https://github.com/zulip/zulip-desktop/pull/1359 dummy text.", expected: "#D1359 dummy text.", expected_undo_texts: ["https://github.com/zulip/zulip-desktop/pull/1359 dummy text."], }, { // Reverse linkify should work for alternative URL templates. paste_html: "", paste_text: "https://github.com/zulip/zulip-desktop/issues/42", expected: "#D42", expected_undo_texts: ["https://github.com/zulip/zulip-desktop/issues/42"], }, ]; for (const test_case of test_cases) { const $textarea = $("textarea#compose-textarea"); // Put the cursor at the start with no selected text. // The URL paste path checks this before it reaches reverse-linkify. $textarea[0] = window.document.createElement("textarea"); $textarea[0].value = ""; inserted_text = undefined; undo_texts = []; const event = { originalEvent: new ClipboardEvent({ clipboardData: { getData(format) { if (format === "text/html") { return test_case.paste_html; } return test_case.paste_text; }, }, }), preventDefault() {}, stopPropagation() {}, }; compose_paste.paste_handler.call($textarea, event, noop); assert.equal(inserted_text, test_case.expected, test_case.paste_text); assert.deepEqual( undo_texts, test_case.expected_undo_texts, `undo texts for: ${test_case.paste_text}`, ); } }); run_test("paste_handler_converter", () => { /* Pasting from another Zulip message */ global.document = window.document; global.window = window; global.Node = window.Node; global.HTMLElement = window.HTMLElement; global.HTMLAnchorElement = window.HTMLAnchorElement; // Bold text let input = '<meta http-equiv="content-type" content="text/html; charset=utf-8"><span style="color: hsl(0, 0%, 13%); font-family: arial, sans-serif; font-size: 12.8px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: hsl(0, 0%, 100%); text-decoration-style: initial; text-decoration-color: initial;"><span> </span>love the<span> </span><b>Zulip</b><b> </b></span><b style="color: hsl(0, 0%, 13%); font-family: arial, sans-serif; font-size: 12.8px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: hsl(0, 0%, 100%); text-decoration-style: initial; text-decoration-color: initial;">Organization</b><span style="color: hsl(0, 0%, 13%); font-family: arial, sans-serif; font-size: 12.8px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: hsl(0, 0%, 100%); text-decoration-style: initial; text-decoration-color: initial;">.</span>'; assert.equal( compose_paste.paste_handler_converter(input), " love the **Zulip** **Organization**.", ); // Inline code input = '<meta http-equiv="content-type" content="text/html; charset=utf-8"><span style="color: hsl(210, 12%, 16%); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: hsl(0, 0%, 100%); text-decoration-style: initial; text-decoration-color: initial; display: inline !important; float: none;">The<span> </span></span><code style="box-sizing: border-box; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 13.6px; padding: 0.2em 0.4em; margin: 0px; background-color: hsla(210, 13%, 12%, 0.05); border-radius: 3px; color: hsl(210, 12%, 16%); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">JSDOM</code><span style="color: hsl(210, 12%, 16%); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: hsl(0, 0%, 100%); text-decoration-style: initial; text-decoration-color: initial; display: inline !important; float: none;"><span> </span>constructor</span>'; assert.equal(compose_paste.paste_handler_converter(input), "The `JSDOM` constructor"); // A python code block input = `<meta http-equiv="content-type" content="text/html; charset=utf-8"><p>zulip code block in python</p><div class="codehilite zulip-code-block" data-code-language="Python"><pre><span></span><code><span class="nb">print</span><span class="p">(</span><span class="s2">"hello"</span><span class="p">)</span>\n<span class="nb">print</span><span class="p">(</span><span class="s2">"world"</span><span class="p">)</span></code></pre></div></meta>`; assert.equal( compose_paste.paste_handler_converter(input), 'zulip code block in python\n\n```Python\nprint("hello")\nprint("world")\n```', ); // Single line in a code block input = '<meta http-equiv="content-type" content="text/html; charset=utf-8"><pre><code>single line</code></pre>'; assert.equal(compose_paste.paste_handler_converter(input), "`single line`"); // No code formatting if the given text area has a backtick at the cursor position input = '<meta http-equiv="content-type" content="text/html; charset=utf-8"><pre><code>single line</code></pre>'; assert.equal( compose_paste.paste_handler_converter(input, { val() { return "e.g. `"; }, 0: { selectionStart: 6, value: "e.g. `", }, length: 1, }), "single line", ); // No code formatting if the given text area has a backtick at the cursor position input = '<meta http-equiv="content-type" content="text/html; charset=utf-8"><pre><code>single line</code></pre>'; assert.equal( compose_paste.paste_handler_converter(input, { val() { return "`e.g. ` ```hi`` ``"; }, 0: { selectionStart: 19, value: "`e.g. ` ```hi`` ``", }, length: 1, }), "single line", ); // No code formatting if the given text area has a opening backtick before the cursor position input = '<meta http-equiv="content-type" content="text/html; charset=utf-8"><pre><code>single line</code></pre>'; assert.equal( compose_paste.paste_handler_converter(input, { val() { return "e.g. ` "; }, 0: { selectionStart: 7, value: "e.g. ` ", }, length: 1, }), "single line", ); // Yes code formatting if the given text area does not have a backtick at the cursor position. input = '<meta http-equiv="content-type" content="text/html; charset=utf-8"><pre><code>single line</code></pre>'; assert.equal( compose_paste.paste_handler_converter(input, { val() { return ""; }, 0: { selectionStart: 0, value: "", }, length: 1, }), "`single line`", ); // Yes code formatting if the given text area closes the code block before the cursor position input = '<meta http-equiv="content-type" content="text/html; charset=utf-8"><pre><code>single line</code></pre>'; assert.equal( compose_paste.paste_handler_converter(input, { val() { return "` e.g. ` "; }, 0: { selectionStart: 9, value: "` e.g. ` ", }, length: 1, }), "`single line`", ); // Yes code formatting if the given text area closes the code block before the cursor position input = '<meta http-equiv="content-type" content="text/html; charset=utf-8"><pre><code>single line</code></pre>'; assert.equal( compose_paste.paste_handler_converter(input, { val() { return "``` e.g. ``` ``hi`` "; }, 0: { selectionStart: 20, value: "``` e.g. ``` ``hi`` ", }, length: 1, }), "`single line`", ); // Raw links without custom text input = '<meta http-equiv="content-type" content="text/html; charset=utf-8"><a href="https://zulip.readthedocs.io/en/latest/subsystems/logging.html" target="_blank" title="https://zulip.readthedocs.io/en/latest/subsystems/logging.html" style="color: hsl(200, 100%, 40%); text-decoration: none; cursor: pointer; font-family: "Source Sans 3", "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: hsl(0, 0%, 100%);">https://zulip.readthedocs.io/en/latest/subsystems/logging.html</a>'; assert.equal( compose_paste.paste_handler_converter(input), "https://zulip.readthedocs.io/en/latest/subsystems/logging.html", ); // Links with custom text input = '<meta http-equiv="content-type" content="text/html; charset=utf-8"><a class="reference external" href="https://zulip.readthedocs.io/en/latest/contributing/contributing.html" style="box-sizing: border-box; color: hsl(283, 39%, 53%); text-decoration: none; cursor: pointer; outline: 0px; font-family: Lato, proxima-nova, "Helvetica Neue", Arial, sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: hsl(0, 0%, 99%);">Contributing guide</a>'; assert.equal( compose_paste.paste_handler_converter(input), "[Contributing guide](https://zulip.readthedocs.io/en/latest/contributing/contributing.html)", ); // Only numbered list (list style retained) input = '<meta http-equiv="content-type" content="text/html; charset=utf-8"><ol><li>text</li></ol>'; assert.equal(compose_paste.paste_handler_converter(input), "1. text"); // Heading input = '<meta http-equiv="content-type" content="text/html; charset=utf-8"><h1 style="box-sizing: border-box; font-size: 2em; margin-top: 0px !important; margin-right: 0px; margin-bottom: 16px; margin-left: 0px; font-weight: 600; line-height: 1.25; padding-bottom: 0.3em; border-bottom: 1px solid hsl(216, 14%, 93%); color: hsl(210, 12%, 16%); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Zulip overview</h1><p>normal text</p>'; assert.equal(compose_paste.paste_handler_converter(input), "# Zulip overview\n\nnormal text"); // Only heading (strip heading style) input = '<meta http-equiv="content-type" content="text/html; charset=utf-8"><h1 style="box-sizing: border-box; font-size: 2em; margin-top: 0px !important; margin-right: 0px; margin-bottom: 16px; margin-left: 0px; font-weight: 600; line-height: 1.25; padding-bottom: 0.3em; border-bottom: 1px solid hsl(216, 14%, 93%); color: hsl(210, 12%, 16%); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Zulip overview</h1>'; assert.equal(compose_paste.paste_handler_converter(input), "Zulip overview"); // Italic text input = '<meta http-equiv="content-type" content="text/html; charset=utf-8">normal text <i style="box-sizing: inherit; color: hsl(0, 0%, 0%); font-family: Verdana, sans-serif; font-size: 15px; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: hsl(0, 0%, 100%); text-decoration-style: initial; text-decoration-color: initial;">This text is italic</i>'; assert.equal(compose_paste.paste_handler_converter(input), "normal text *This text is italic*"); // Strikethrough text input = '<meta http-equiv="content-type" content="text/html; charset=utf-8">normal text <del style="box-sizing: inherit; color: hsl(0, 0%, 0%); font-family: Verdana, sans-serif; font-size: 15px; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: hsl(0, 0%, 100%); text-decoration-style: initial; text-decoration-color: initial;">This text is struck through</del>'; assert.equal( compose_paste.paste_handler_converter(input), "normal text ~~This text is struck through~~", ); // Emojis input = '<meta http-equiv="content-type" content="text/html; charset=utf-8"><span style="color: rgb(221, 222, 238); font-family: "Source Sans 3", sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(33, 45, 59); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; display: inline !important; float: none;">emojis:<span> </span></span><span aria-label="smile" class="emoji emoji-1f604" role="img" title="smile" style="height: 20px; width: 20px; position: relative; margin-top: -7px; vertical-align: middle; top: 3px; background-position: 55% 46.667%; display: inline-block; background-image: url("http://localhost:9991/webpack/files/generated/emoji/google.webp"); background-size: 6100%; background-repeat: no-repeat; text-indent: 100%; white-space: nowrap; overflow: hidden; color: rgb(221, 222, 238); font-family: "Source Sans 3", sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(33, 45, 59); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">:smile:</span><span style="color: rgb(221, 222, 238); font-family: "Source Sans 3", sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(33, 45, 59); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; display: inline !important; float: none;"><span> </span></span><span aria-label="family man woman girl" class="emoji emoji-1f468-200d-1f469-200d-1f467" role="img" title="family man woman girl" style="height: 20px; width: 20px; position: relative; margin-top: -7px; vertical-align: middle; top: 3px; background-position: 23.333% 75%; display: inline-block; background-image: url("http://localhost:9991/webpack/files/generated/emoji/google.webp"); background-size: 6100%; background-repeat: no-repeat; text-indent: 100%; white-space: nowrap; overflow: hidden; color: rgb(221, 222, 238); font-family: "Source Sans 3", sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(33, 45, 59); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">:family_man_woman_girl:</span>'; assert.equal( compose_paste.paste_handler_converter(input), "emojis: :smile: :family_man_woman_girl:", ); // Nested lists input = '<meta http-equiv="content-type" content="text/html; charset=utf-8"><ul style="padding: 0px; margin: 0px 0px 5px 20px; color: rgb(221, 222, 238); font-family: "Source Sans 3", sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(33, 45, 59); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><li style="line-height: inherit;">bulleted</li><li style="line-height: inherit;">nested<ul style="padding: 0px; margin: 2px 0px 5px 20px;"><li style="line-height: inherit;">nested level 1</li><li style="line-height: inherit;">nested level 1 continue<ul style="padding: 0px; margin: 2px 0px 5px 20px;"><li style="line-height: inherit;">nested level 2</li><li style="line-height: inherit;">nested level 2 continue</li></ul></li></ul></li></ul>'; assert.equal( compose_paste.paste_handler_converter(input), "* bulleted\n* nested\n * nested level 1\n * nested level 1 continue\n * nested level 2\n * nested level 2 continue", ); // Heading from https://arxiv.org/abs/1301.3191 input = '<html><body><!--StartFragment--><h1 class="title mathjax" style="font-size: 1.8em !important; font-weight: 700; line-height: 27.9936px; display: block; margin-block: 12px; margin: 0.25em 0px 12px 20px; margin-inline: 20px 0px; color: rgb(0, 0, 0); font-family: "Lucida Grande", Helvetica, Arial, sans-serif; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">Enriched categories as a free cocompletion</h1><br class="Apple-interchange-newline"><!--EndFragment--></body></html>'; assert.equal( compose_paste.paste_handler_converter(input), "Enriched categories as a free cocompletion", ); // Heading from https://www.sciencedirect.com/science/article/pii/S0001870815004715 input = '<html><body><!--StartFragment--><h2 class="section-title u-h4 u-margin-l-top u-margin-xs-bottom" style="box-sizing: border-box; margin-top: 32px !important; margin-right: 0px; margin-bottom: 8px !important; margin-left: 0px; padding: 0px; font-weight: normal !important; font-size: 20px !important; line-height: var(--sd-ui-line-height) !important; color: rgb(31, 31, 31); font-family: ElsevierGulliver, Georgia, "Times New Roman", Times, STIXGeneral, "Cambria Math", "Lucida Sans Unicode", "Microsoft Sans Serif", "Segoe UI Symbol", "Arial Unicode MS", serif, sans-serif; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">Abstract</h2><div id="as0010" style="box-sizing: border-box; margin: 0px; padding: 0px; color: rgb(31, 31, 31); font-family: ElsevierGulliver, Georgia, "Times New Roman", Times, STIXGeneral, "Cambria Math", "Lucida Sans Unicode", "Microsoft Sans Serif", "Segoe UI Symbol", "Arial Unicode MS", serif, sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><br class="Apple-interchange-newline"><!--EndFragment--></body></html>'; assert.equal(compose_paste.paste_handler_converter(input), "Abstract"); // Heading from https://en.wikipedia.org/wiki/James_Madison input = '<html><body><!--StartFragment--><h1 id="firstHeading" class="firstHeading mw-first-heading" style="color: var(--color-emphasized,#101418); font-weight: normal; margin: 0px; padding: 0px; display: flow-root; word-break: break-word; border: 0px; font-size: 1.8em; font-family: "Linux Libertine", Georgia, Times, "Source Serif Pro", serif; line-height: 1.375; overflow-wrap: break-word; flex-grow: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span class="mw-page-title-main">James Madison</span></h1><div id="p-lang-btn" class="vector-dropdown mw-portlet mw-portlet-lang" style="position: relative; float: right; box-sizing: border-box; flex-shrink: 0; margin-right: -12px; color: rgb(32, 33, 34); font-family: sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"></div><br class="Apple-interchange-newline"><!--EndFragment--></body></html>'; assert.equal(compose_paste.paste_handler_converter(input), "James Madison"); // Heading from https://customer-identity-access-management.hashnode.dev/from-words-to-vectors-understanding-the-magic-of-text-embedding input = `<html><body><!--StartFragment--><div class="mt-6 break-words px-4 text-center font-heading text-3xl font-bold text-slate-900 dark:text-white md:mt-10 md:px-5 md:text-4xl lg:px-8 xl:px-20 xl:text-5xl mb-8 md:mb-14" style="box-sizing: border-box; ..."> <h1 class="leading-tight" data-query="post-title">From Words to Vectors: Understanding the Magic of Text Embedding</h1> </div></body></html>`; assert.equal( compose_paste.paste_handler_converter(input), "From Words to Vectors: Understanding the Magic of Text Embedding", ); // Check we don't double-convert HTML to text. input = `<del>turtles are cool</del>`; assert.equal(compose_paste.paste_handler_converter(input), "turtles are cool"); input = `<span style="color: rgb(31, 35, 40); font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; font-size: 11.9px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: break-spaces; background-color: rgba(129, 139, 152, 0.12); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; display: inline !important; float: none;"><del>turtles are cool</del></span>`; assert.equal(compose_paste.paste_handler_converter(input), "<del>turtles are cool</del>"); // 2 paragraphs with line break/s in between input = '<meta http-equiv="content-type" content="text/html; charset=utf-8"><p>paragraph 1</p><br><p>paragraph 2</p>'; assert.equal(compose_paste.paste_handler_converter(input), "paragraph 1\n\nparagraph 2"); // Pasting from external sources // Pasting list from GitHub input = '<div class="preview-content"><div class="comment"><div class="comment-body markdown-body js-preview-body" style="min-height: 131px;"><p>Test list:</p><ul><li>Item 1</li><li>Item 2</li></ul></div></div></div>'; assert.equal(compose_paste.paste_handler_converter(input), "Test list:\n* Item 1\n* Item 2"); // Pasting list from VS Code input = '<div class="ace-line gutter-author-d-iz88z86z86za0dz67zz78zz78zz74zz68zjz80zz71z9iz90za3z66zs0z65zz65zq8z75zlaz81zcz66zj6g2mz78zz76zmz66z22z75zfcz69zz66z ace-ltr focused-line" dir="auto" id="editor-3-ace-line-41"><span>Test list:</span></div><div class="ace-line gutter-author-d-iz88z86z86za0dz67zz78zz78zz74zz68zjz80zz71z9iz90za3z66zs0z65zz65zq8z75zlaz81zcz66zj6g2mz78zz76zmz66z22z75zfcz69zz66z line-list-type-bullet ace-ltr" dir="auto" id="editor-3-ace-line-42"><ul class="listtype-bullet listindent1 list-bullet1"><li><span class="ace-line-pocket-zws" data-faketext="" data-contentcollector-ignore-space-at="end"></span><span class="ace-line-pocket" data-faketext="" contenteditable="false"></span><span class="ace-line-pocket-zws" data-faketext="" data-contentcollector-ignore-space-at="start"></span><span>Item 1</span></li></ul></div><div class="ace-line gutter-author-d-iz88z86z86za0dz67zz78zz78zz74zz68zjz80zz71z9iz90za3z66zs0z65zz65zq8z75zlaz81zcz66zj6g2mz78zz76zmz66z22z75zfcz69zz66z line-list-type-bullet ace-ltr" dir="auto" id="editor-3-ace-line-43"><ul class="listtype-bullet listindent1 list-bullet1"><li><span class="ace-line-pocket-zws" data-faketext="" data-contentcollector-ignore-space-at="end"></span><span class="ace-line-pocket" data-faketext="" contenteditable="false"></span><span class="ace-line-pocket-zws" data-faketext="" data-contentcollector-ignore-space-at="start"></span><span>Item 2</span></li></ul></div>'; assert.equal(compose_paste.paste_handler_converter(input), "Test list:\n* Item 1\n* Item 2"); // Pasting markdown from VS Code where each line is wrapped by a <div> shouldn't insert an extra newline. input = `<html>\r\n<body>\r\n<!--StartFragment--><div style="color: #cccccc;background-color: #1f1f1f;font-family: 'Fira Code Retina', Consolas, 'Courier New', monospace;font-weight: 350;font-size: 12.852px;line-height: 17px;white-space: pre;"><div><span style="color: #cccccc;">authentication-methods</span></div><div><span style="color: #cccccc;">export-and-import</span></div><div><span style="color: #cccccc;">postgresql</span></div><div><span style="color: #cccccc;">upload-backends</span></div><div><span style="color: #cccccc;">ssl-certificates</span></div><div><span style="color: #cccccc;">email</span></div></div><!--EndFragment-->\r\n</body>\r\n</html>`; assert.equal( compose_paste.paste_handler_converter(input), "authentication-methods\nexport-and-import\npostgresql\nupload-backends\nssl-certificates\nemail", ); // Pasting from Google Sheets (remove <style> elements completely) input = '<meta http-equiv="content-type" content="text/html; charset=utf-8"><style type="text/css"><!--td {border: 1px solid #cccccc;}br {mso-data-placement:same-cell;}--></style><span style="font-size:10pt;font-family:Arial;font-style:normal;text-align:right;" data-sheets-value="{"1":3,"3":123}" data-sheets-userformat="{"2":769,"3":{"1":0},"11":3,"12":0}">123</span>'; assert.equal(compose_paste.paste_handler_converter(input), "123"); // Pasting a long, visually line-wrapped single-line message from Firefox should not insert extraneous newlines. input = `<html><body>\n<!--StartFragment--><div class="message_content rendered_markdown">\n<p>At some point recently, Zulip changed such that copying a \nlong message includes hard newlines, rather than putting things all on \none line when they were on one line in the original message.</p>\n</div><!--EndFragment-->\n</body>\n</html>`; assert.equal( compose_paste.paste_handler_converter(input), "At some point recently, Zulip changed such that copying a long message includes hard newlines, rather than putting things all on one line when they were on one line in the original message.", ); // Pasting from Excel input = `<html xmlns:v="urn:schemas-microsoft-com:vml"\nxmlns:o="urn:schemas-microsoft-com:office:office"\nxmlns:x="urn:schemas-microsoft-com:office:excel"\nxmlns="http://www.w3.org/TR/REC-html40">\n<head>\n<meta http-equiv=Content-Type content="text/html; charset=utf-8">\n<meta name=ProgId content=Excel.Sheet>\n<meta name=Generator content="Microsoft Excel 15">\n<link id=Main-File rel=Main-File\nhref="file:///C:/Users/ADMINI~1/AppData/Local/Temp/msohtmlclip1/01/clip.htm">\n<link rel=File-List\nhref="file:///C:/Users/ADMINI~1/AppData/Local/Temp/msohtmlclip1/01/clip_filelist.xml">\n<style>\n<!--table\n {mso-displayed-decimal-separator:"\\.";\n mso-displayed-thousand-separator:"\\,";}\n@page\n {margin:.75in .7in .75in .7in;\n mso-header-margin:.3in;\n mso-footer-margin:.3in;}\ntr\n {mso-height-source:auto;}\ncol\n {mso-width-source:auto;}\nbr\n {mso-data-placement:same-cell;}\ntd\n {padding-top:1px;\n padding-right:1px;\n padding-left:1px;\n mso-ignore:padding;\n color:black;\n font-size:11.0pt;\n font-weight:400;\n font-style:normal;\n text-decoration:none;\n font-family:Calibri, sans-serif;\n mso-font-charset:0;\n mso-number-format:General;\n text-align:general;\n vertical-align:bottom;\n border:none;\n mso-background-source:auto;\n mso-pattern:auto;\n mso-protection:locked visible;\n white-space:nowrap;\n mso-rotate:0;}\n.xl65\n {mso-number-format:"_\\(\\0022$\\0022* \\#\\,\\#\\#0\\.00_\\)\\;_\\(\\0022$\\0022* \\\\\\(\\#\\,\\#\\#0\\.00\\\\\\)\\;_\\(\\0022$\\0022* \\0022-\\0022??_\\)\\;_\\(\\@_\\)";}\n-->\n</style>\n</head>\n<body link="#0563C1" vlink="#954F72">\n<table border=0 cellpadding=0 cellspacing=0 width=88 style='border-collapse:\n collapse;width:66pt'>\n<!--StartFragment-->\n <col width=88 style='mso-width-source:userset;mso-width-alt:3218;width:66pt'>\n <tr height=20 style='height:15.0pt'>\n <td height=20 class=xl65 width=88 style='height:15.0pt;width:66pt;font-size:\n 11.0pt;color:black;font-weight:400;text-decoration:none;text-underline-style:\n none;text-line-through:none;font-family:Calibri, sans-serif;border-top:.5pt solid #5B9BD5;\n border-right:none;border-bottom:none;border-left:none'><span\n style='mso-spacerun:yes'> </span>$<span style='mso-spacerun:yes'>\n </span>20.00 </td>\n </tr>\n <tr height=20 style='height:15.0pt'>\n <td height=20 class=xl65 style='height:15.0pt;font-size:11.0pt;color:black;\n font-weight:400;text-decoration:none;text-underline-style:none;text-line-through:\n none;font-family:Calibri, sans-serif;border-top:.5pt solid #5B9BD5;\n border-right:none;border-bottom:none;border-left:none'><span\n style='mso-spacerun:yes'> </span>$<span\n style='mso-spacerun:yes'> </span>7.00 </td>\n </tr>\n<!--EndFragment-->\n</table>\n</body>\n</html>`; // Pasting from Excel using ^V should paste an image. assert.ok(compose_paste.is_single_image(input)); // Pasting from Excel using ^⇧V should paste formatted text. assert.equal(compose_paste.paste_handler_converter(input), " \n\n$ 20.00\n\n$ 7.00"); // Pasting from LibreOffice Calc should paste an image. input = `<html><head><meta http-equiv="content-type" content="text/html; charset=utf-8"><title></title><meta name="generator" content="LibreOffice 25.2.3.2 (Windows)"><style type="text/css"> body,div,table,thead,tbody,tfoot,tr,th,td,p { font-family:"Arial"; font-size:x-small } a.comment-indicator:hover + comment { background:#ffd; position:absolute; display:block; border:1px solid black; padding:0.5em; } a.comment-indicator { background:red; display:inline-block; border:1px solid black; width:0.5em; height:0.5em; } comment { display:none; } </style></head><body><table cellspacing="0" border="0"><colgroup span="4" width="107"></colgroup><tbody><tr><td height="24" align="left" data-sheets-value="{ "1": 2, "2": "Kathleen"}">Kathleen</td><td align="left" data-sheets-value="{ "1": 2, "2": "Hanner"}">Hanner</td><td align="left" data-sheets-value="{ "1": 2, "2": "Female"}">Female</td><td align="left" data-sheets-value="{ "1": 2, "2": "United States"}">United States</td></tr><tr><td height="24" align="left" data-sheets-value="{ "1": 2, "2": "Nereida"}">Nereida</td><td align="left" data-sheets-value="{ "1": 2, "2": "Magwood"}">Magwood</td><td align="left" data-sheets-value="{ "1": 2, "2": "Female"}">Female</td><td align="left" data-sheets-value="{ "1": 2, "2": "United States"}">United States</td></tr></tbody></table></body></html>`; assert.ok(compose_paste.is_single_image(input)); // Pastes containing the `data-message-header-paragraph="true"` attribute should // be formatted as per the custom turndown rule. The empty paragraph that // copy_messages inserts between consecutive messages from the same recipient // should render as a blank line separating them. input = `<html><body><!--StartFragment--><p data-message-header-paragraph="true"><strong>test > LAST API has been skipping erratically </strong><span>| Dec 20</span></p><b>Othello, the Moor of Venice: </b><div>When shall we three meet again In thunder, lightning, or in rain?</div><p></p><b>Othello, the Moor of Venice: </b><div>The second consecutive message.</div><p data-message-header-paragraph="true"><strong>test > leased keurig wasn't loading </strong><span>| Dec 21</span></p><b>King Hamlet: </b><div>Sit down awhile; And let us once again assail your ears, That are so fortified against our story What we have two nights seen.</div><!--EndFragment--></body></html>`; assert.equal( compose_paste.paste_handler_converter(input), "**test > LAST API has been skipping erratically** | Dec 20\n**Othello, the Moor of Venice:**\nWhen shall we three meet again In thunder, lightning, or in rain?\n\n**Othello, the Moor of Venice:**\nThe second consecutive message.\n\n**test > leased keurig wasn't loading** | Dec 21\n**King Hamlet:**\nSit down awhile; And let us once again assail your ears, That are so fortified against our story What we have two nights seen.", ); // Copying a message that is a paragraph followed by a list: copy_messages // unwraps only the leading paragraph into a div (so the sender name is // separated from the body by a single newline), leaving the list intact. // The list must stay on its own line and not get glued onto the paragraph. input = `<html><body><!--StartFragment--><b>Desdemona: </b><div>intro text</div>\n<ul>\n<li>item one</li>\n<li>item two</li>\n<li>item three</li>\n</ul><!--EndFragment--></body></html>`; assert.equal( compose_paste.paste_handler_converter(input), "**Desdemona:**\nintro text\n* item one\n* item two\n* item three", ); // Partially selecting the first message inserts the "..." ellipsis inside // the leading paragraph, so it stays glued to the start of the truncated // text (on the same line) rather than landing on its own line. input = `<html><body><!--StartFragment--><b>Iago: </b><div><span>...</span>of Nephelococcygia</div>\n<ul>\n<li>Myrmica ruginodis</li>\n<li>Myrmica kotokui</li>\n</ul><!--EndFragment--></body></html>`; assert.equal( compose_paste.paste_handler_converter(input), "**Iago:**\n...of Nephelococcygia\n* Myrmica ruginodis\n* Myrmica kotokui", ); // This contains three child elements inside the body tag, pasted // from LibreOffice Writer, which is correctly classified as not an image. input = `<html><head><meta http-equiv="content-type" content="text/html; charset=utf-8"/><title></title><meta name="generator" content="LibreOffice 25.2.3.2 (Windows)"/><style type="text/css">@page { size: 8.5in 11in; margin: 0.79in }td p { orphans: 0; widows: 0; background: transparent }p { line-height: 115%; margin-bottom: 0.1in; background: transparent }</style></head><body lang="en-US" link="#000080" vlink="#800000" dir="ltr"><p style="line-height: 100%; margin-bottom: 0in">ello world</p><table width="100%" cellpadding="0" cellspacing="0"><col width="51*"/><col width="51*"/><col width="51*"/><col width="51*"/><col width="51*"/><tr valign="top"><td width="20%" style="border: none; padding: 0in"><p>X</p></td><td width="20%" style="border: none; padding: 0in"><p>as</p></td><td width="20%" style="border: none; padding: 0in"><p>Jak</p></td><td width="20%" style="border: none; padding: 0in"><p>J</p></td><td width="20%" style="border: none; padding: 0in"><p>Nm</p></td></tr><tr valign="top"><td width="20%" style="border: none; padding: 0in"><p>,mn</p></td><td width="20%" style="border: none; padding: 0in"><p>,nnf</p></td><td width="20%" style="border: none; padding: 0in"><p>Adlk</p></td><td width="20%" style="border: none; padding: 0in"><p>Asn</p></td><td width="20%" style="border: none; padding: 0in"><p>,amns</p></td></tr><tr valign="top"><td width="20%" style="border: none; padding: 0in"><p>Nm</p></td><td width="20%" style="border: none; padding: 0in"><p>Oi</p></td><td width="20%" style="border: none; padding: 0in"><p>Poi</p></td><td width="20%" style="border: none; padding: 0in"><p>B</p></td><td width="20%" style="border: none; padding: 0in"><p>Ijo</p></td></tr><tr valign="top"><td width="20%" style="border: none; padding: 0in"><p>,mn,</p></td><td width="20%" style="border: none; padding: 0in"><p>;ih</p></td><td width="20%" style="border: none; padding: 0in"><p>Oug</p></td><td width="20%" style="border: none; padding: 0in"><p>Iu</p></td><td width="20%" style="border: none; padding: 0in"><p>G</p></td></tr><tr valign="top"><td width="20%" style="border: none; padding: 0in"><p>Ug</p></td><td width="20%" style="border: none; padding: 0in"><p>Bkjb</p></td><td width="20%" style="border: none; padding: 0in"><p>Kjbk</p></td><td width="20%" style="border: none; padding: 0in"><p>;jbj</p></td><td width="20%" style="border: none; padding: 0in"><p>;jb;</p></td></tr><tr valign="top"><td width="20%" style="border: none; padding: 0in"><p>Bkjb</p></td><td width="20%" style="border: none; padding: 0in"><p>Ugug</p></td><td width="20%" style="border: none; padding: 0in"><p>I9</p></td><td width="20%" style="border: none; padding: 0in"><p>68</p></td><td width="20%" style="border: none; padding: 0in"><p>0</p></td></tr><tr valign="top"><td width="20%" style="border: none; padding: 0in"><p>90kjb</p></td><td width="20%" style="border: none; padding: 0in"><p>,bnbiu</p></td><td width="20%" style="border: none; padding: 0in"><p>Ofif</p></td><td width="20%" style="border: none; padding: 0in"><p>P8gp</p></td><td width="20%" style="border: none; padding: 0in"><p>pugp</p></td></tr></table><p style="line-height: 100%; margin-bottom: 0in"><br/></p></body></html>`; assert.ok(!compose_paste.is_single_image(input)); // <body> has a single child element which is not a <table> pasted // from LibreOffice Writer should get pasted normally. input = `<html><head><meta http-equiv="content-type" content="text/html; charset=utf-8"/><title></title><meta name="generator" content="LibreOffice 25.2.3.2 (Windows)"/><style type="text/css">@page { size: 8.5in 11in; margin: 0.79in }p { line-height: 115%; margin-bottom: 0.1in; background: transparent }</style></head><body lang="en-US" link="#000080" vlink="#800000" dir="ltr"><p style="line-height: 100%; margin-bottom: 0in">Hello world this is some random text.</p></body></html>`; assert.ok(!compose_paste.is_single_image(input)); // A single table pasted from LibreOffice Writer is incorrectly // detected as a LibreOffice Calc table. // See https://github.com/zulip/zulip/pull/34752/#discussion_r2113598064 input = `<html><head><meta http-equiv="content-type" content="text/html; charset=utf-8"/><title></title><meta name="generator" content="LibreOffice 25.2.3.2 (Windows)"/><style type="text/css">@page { size: 8.5in 11in; margin: 0.79in }p { line-height: 115%; margin-bottom: 0.1in; background: transparent }</style></head><body lang="en-US" link="#000080" vlink="#800000" dir="ltr"><table width="258" cellpadding="2" cellspacing="0"><col width="83"/><col width="81"/><col width="81"/><tr valign="bottom"><td width="83" height="16" style="border: none; padding: 0in"><p align="left"><font face="Arial, serif">Melgar</font></p></td><td width="81" style="border: none; padding: 0in"><p align="left"><font face="Arial, serif">Female</font></p></td><td width="81" style="border: none; padding: 0in"><p align="left"><font face="Arial, serif">UnitedStates</font></p></td></tr><tr valign="bottom"><td width="83" height="16" style="border: none; padding: 0in"><p align="left"><font face="Arial, serif">Weiland</font></p></td><td width="81" style="border: none; padding: 0in"><p align="left"><font face="Arial, serif">Female</font></p></td><td width="81" style="border: none; padding: 0in"><p align="left"><font face="Arial, serif">UnitedStates</font></p></td></tr><tr valign="bottom"><td width="83" height="16" style="border: none; padding: 0in"><p align="left"><font face="Arial, serif">Winward</font></p></td><td width="81" style="border: none; padding: 0in"><p align="left"><font face="Arial, serif">Female</font></p></td><td width="81" style="border: none; padding: 0in"><p align="left"><font face="Arial, serif">GreatBritain</font></p></td></tr></table></body></html>`; assert.ok(compose_paste.is_single_image(input)); // Copying an image and pasting it with `paste_html` containing other empty text nodes and // comment nodes should still classify the `paste_html` as an image that needs to be uploaded. input = `<html>\n<body>\n<!--StartFragment--><img src="http://zulip.zulipdev.com:9991/user_uploads/thumbnail/2/a0/wETeF-Yv2wmMM6289kO9VC0s/image.png/840x560.webp"/><!--EndFragment-->\n</body>\n</html>`; assert.ok(compose_paste.is_single_image(input)); // Pasting from the mac terminal input = '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"><html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><meta http-equiv="Content-Style-Type" content="text/css"><title></title><meta name="Generator" content="Cocoa HTML Writer"><meta name="CocoaVersion" content="2575.4"><style type="text/css">p.p1 {margin: 0.0px 0.0px 0.0px 0.0px; font: 11.0px Menlo; color: #000000}span.s1 {font-variant-ligatures: no-common-ligatures}</style></head><body><p class="p1"><span class="s1">insertions</span></p></body></html>'; assert.equal(compose_paste.paste_handler_converter(input), "insertions"); // Zulip timestamp followed by text. input = '<meta charset=\'utf-8\'><time datetime="2026-05-29T16:30:00Z" style="background: rgba(0, 0, 0, 0.2); border-radius: 3px; box-shadow: rgba(0, 0, 0, 0.4) 0px 0px 0px 1px; white-space: nowrap; margin: 0px 2px; font-size: 0.95em; color: rgb(222, 222, 222); font-family: "Source Sans 3 VF", sans-serif;"><span class="timestamp-content-wrapper" style="font-size: 1.0526em; padding: 0px 0.2em; display: inline-flex; align-items: baseline; gap: 3px;"><span></span>May 29, 2026, 10:00 PM</span></time><span style="color: rgb(222, 222, 222); display: inline !important; float: none;"><span> </span>good better<span> </span></span>'; assert.equal( compose_paste.paste_handler_converter(input), "<time:2026-05-29T16:30:00Z> good better", ); // Chrome-stripped timestamp: Chrome's clipboard serializer removes the <time> // wrapper, leaving only the clock icon <i> and the <span data-datetime> fallback. input = '<meta charset=\'utf-8\'><br class="Apple-interchange-newline"><i class="zulip-icon zulip-icon-clock markdown-timestamp-icon" style="text-transform: none; font-size: 15.9995px; line-height: 15.9995px; text-decoration-line: inherit; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; display: inline-block; speak: none; font-family: zulip-icons !important; font-style: normal !important; font-weight: 400; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-numeric: normal !important; font-variant-east-asian: normal !important; font-variant-alternates: normal !important; font-variant-position: normal !important; font-variant-emoji: normal !important; align-self: center; color: rgb(222, 222, 222); letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: nowrap; background-color: rgba(0, 0, 0, 0.2);"></i><span data-datetime="2026-05-23T17:30:00Z" style="color: rgb(222, 222, 222); font-family: "Source Sans 3 VF", sans-serif; font-size: 15.9995px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: nowrap; background-color: rgba(0, 0, 0, 0.2); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">Sat, May 23, 2026, 11:00 PM</span>'; assert.equal(compose_paste.paste_handler_converter(input), "<time:2026-05-23T17:30:00Z>"); // Non-Zulip <time datetime> elements, should not be converted. input = "<meta charset='utf-8'><time datetime=\"2024-06-01T09:00:00Z\">June 1, 2024</time>"; assert.equal(compose_paste.paste_handler_converter(input), "June 1, 2024"); // Math block tests /* This first batch of math block tests uses captured fixtures (`input`). This lets us verify behavior like the empty `.katex-display` divs in case of newlines in the `original_markdown` See https://github.com/zulip/zulip/pull/32629#discussion_r1883810127 */ for (const math_block_test of katex_tests.math_block_tests) { input = math_block_test.input; assert.equal(compose_paste.paste_handler_converter(input), math_block_test.expected_output); } // This next batch of tests round-trips the LaTeX syntax through // the Markdown processor and then the paste handler. const dummy_helper_config = { should_translate_emoticons: () => false, get_linkifier_map: () => new Map(), }; assert.equal(dummy_helper_config.should_translate_emoticons(), false); assert.deepEqual(dummy_helper_config.get_linkifier_map(), new Map()); for (const inline_math_expression_test of katex_tests.inline_math_expression_tests) { const paste_html = parse({ raw_content: inline_math_expression_test.original_markup, helper_config: dummy_helper_config, }).content; assert.equal( compose_paste.paste_handler_converter(paste_html), inline_math_expression_test.expected_output, ); } for (const span_conversion_test of katex_tests.text_node_to_span_conversion_tests) { const paste_html = parse({ raw_content: span_conversion_test.original_markup, helper_config: dummy_helper_config, }).content; assert.equal( compose_paste.paste_handler_converter(paste_html), span_conversion_test.expected_output, ); } const meta = '<meta http-equiv="content-type" content="text/html; charset=utf-8">'; let firefox_input; // Non-silent user mention firefox_input = meta + '<p><span class="user-mention" data-user-id="10">' + '<span class="mention-content-wrapper">@King Hamlet</span></span></p>'; assert.equal(compose_paste.paste_handler_converter(firefox_input), "@**King Hamlet|10**"); // Guest user mention: rendered_markdown.ts adds a "(guest)" suffix // to the display name (commit 70c9d0765f). The people-store lookup // returns the canonical full_name, so the suffix is dropped naturally. firefox_input = meta + '<p><span class="user-mention" data-user-id="13">' + '<span class="mention-content-wrapper">@Polonius (guest)</span></span></p>'; assert.equal(compose_paste.paste_handler_converter(firefox_input), "@**Polonius|13**"); // Silent user mention firefox_input = meta + '<p><span class="user-mention silent" data-user-id="10">' + '<span class="mention-content-wrapper">King Hamlet</span></span></p>'; assert.equal(compose_paste.paste_handler_converter(firefox_input), "@_**King Hamlet|10**"); // Self-mention adds a `user-mention-me` modifier class. firefox_input = meta + '<p><span class="user-mention user-mention-me" data-user-id="9">' + '<span class="mention-content-wrapper">@Desdemona</span></span></p>'; assert.equal(compose_paste.paste_handler_converter(firefox_input), "@**Desdemona|9**"); // Non-silent group mention (single-asterisk syntax) firefox_input = meta + '<p><span class="user-group-mention" data-user-group-id="24">' + '<span class="mention-content-wrapper">@hamletcharacters</span></span></p>'; assert.equal(compose_paste.paste_handler_converter(firefox_input), "@*hamletcharacters*"); // Silent group mention firefox_input = meta + '<p><span class="user-group-mention silent" data-user-group-id="24">' + '<span class="mention-content-wrapper">hamletcharacters</span></span></p>'; assert.equal(compose_paste.paste_handler_converter(firefox_input), "@_*hamletcharacters*"); // System group mention: the pill's display text ("Owners") differs from // the mention name ("role:owners"), so we resolve the name by group id. firefox_input = meta + '<p><span class="user-group-mention" data-user-group-id="25">' + '<span class="mention-content-wrapper">@Owners</span></span></p>'; assert.equal(compose_paste.paste_handler_converter(firefox_input), "@*role:owners*"); // Non-silent topic mention firefox_input = meta + '<p><span class="topic-mention">' + '<span class="mention-content-wrapper">@topic</span></span></p>'; assert.equal(compose_paste.paste_handler_converter(firefox_input), "@**topic**"); // Silent topic mention firefox_input = meta + '<p><span class="topic-mention silent">' + '<span class="mention-content-wrapper">topic</span></span></p>'; assert.equal(compose_paste.paste_handler_converter(firefox_input), "@_**topic**"); // Non-silent channel wildcard mention (no |user_id suffix) firefox_input = meta + '<p><span class="user-mention channel-wildcard-mention" data-user-id="*">' + '<span class="mention-content-wrapper">@all</span></span></p>'; assert.equal(compose_paste.paste_handler_converter(firefox_input), "@**all**"); // Silent channel wildcard mention firefox_input = meta + '<p><span class="user-mention channel-wildcard-mention silent" data-user-id="*">' + '<span class="mention-content-wrapper">all</span></span></p>'; assert.equal(compose_paste.paste_handler_converter(firefox_input), "@_**all**"); // Mention inline with surrounding text. firefox_input = meta + '<p>Hey <span class="user-mention" data-user-id="10">' + '<span class="mention-content-wrapper">@King Hamlet</span></span>, how are you?</p>'; assert.equal( compose_paste.paste_handler_converter(firefox_input), "Hey @**King Hamlet|10**, how are you?", ); // Lone mention without ancestor wrapper (bare span), as can happen // when keyboard selection contains only the mention element. firefox_input = meta + '<span class="user-mention" data-user-id="10">' + '<span class="mention-content-wrapper">@King Hamlet</span></span>'; assert.equal(compose_paste.paste_handler_converter(firefox_input), "@**King Hamlet|10**"); // Unknown user (not in the people store) falls back to the id-only // @**|id** syntax; the backend resolves it on send. firefox_input = meta + '<p><span class="user-mention" data-user-id="9999">' + '<span class="mention-content-wrapper">@Unknown</span></span></p>'; assert.equal(compose_paste.paste_handler_converter(firefox_input), "@**|9999**"); let chrome_input; // Non-silent user mention. chrome_input = `<html><body><!--StartFragment--><span class="user-mention" data-user-id="10" style="padding: 0px 3px; border-radius: 3px; white-space: nowrap; font-size: 0.95em; color: rgba(255, 255, 255, 0.8); background-color: rgba(100, 100, 206, 0.25); cursor: pointer; font-family: "Source Sans 3 VF", sans-serif; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span class="mention-content-wrapper" style="font-size: 1.0526em;">@King Hamlet</span></span><span style="color: rgb(222, 222, 222); font-family: "Source Sans 3 VF", sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(31, 40, 40); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; display: inline !important; float: none;">, how are you?</span><!--EndFragment--></body></html>`; assert.equal( compose_paste.paste_handler_converter(chrome_input), "@**King Hamlet|10**, how are you?", ); // Silent user mention. chrome_input = `<html><body><!--StartFragment--><span class="user-mention silent" data-user-id="10" style="padding: 0px 3px; border-radius: 3px; white-space: nowrap; font-size: 0.95em; color: rgba(255, 255, 255, 0.8); background-color: rgba(100, 100, 206, 0.45); cursor: pointer; font-family: "Source Sans 3 VF", sans-serif; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span class="mention-content-wrapper" style="font-size: 1.0526em;">King Hamlet</span></span><span style="color: rgb(222, 222, 222); font-family: "Source Sans 3 VF", sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(31, 40, 40); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; display: inline !important; float: none;">, silent test</span><!--EndFragment--></body></html>`; assert.equal( compose_paste.paste_handler_converter(chrome_input), "@_**King Hamlet|10**, silent test", ); // Self-mention adds a user-mention-me modifier class. chrome_input = `<html><body><!--StartFragment--><span class="user-mention user-mention-me" data-user-id="9" style="padding: 0px 3px; border-radius: 3px; white-space: nowrap; font-size: 0.95em; color: rgb(194, 194, 255); background-color: rgba(100, 100, 206, 0.45); cursor: pointer; font-weight: 600; font-family: "Source Sans 3 VF", sans-serif; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span class="mention-content-wrapper" style="font-size: 1.0526em;">@Desdemona</span></span><span style="color: rgb(222, 222, 222); font-family: "Source Sans 3 VF", sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(35, 35, 46); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; display: inline !important; float: none;"> hello</span><!--EndFragment--></body></html>`; assert.equal(compose_paste.paste_handler_converter(chrome_input), "@**Desdemona|9** hello"); // Guest user mention: rendered_markdown.ts adds a "(guest)" suffix to // the display name (commit 70c9d0765f). The people-store lookup returns // the canonical full_name, so the suffix is dropped naturally. chrome_input = `<html><body><!--StartFragment--><span class="user-mention" data-user-id="13" style="padding: 0px 3px; border-radius: 3px; white-space: nowrap; font-size: 0.95em; color: rgba(255, 255, 255, 0.8); background-color: rgba(100, 100, 206, 0.45); cursor: pointer; font-family: "Source Sans 3 VF", sans-serif; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span class="mention-content-wrapper" style="font-size: 1.0526em;">@Polonius (guest)</span></span><span style="color: rgb(222, 222, 222); font-family: "Source Sans 3 VF", sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(34, 34, 34); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; display: inline !important; float: none;"><span> </span>guest</span><!--EndFragment--></body></html>`; assert.equal(compose_paste.paste_handler_converter(chrome_input), "@**Polonius|13** guest"); // Non-silent group mention (single-asterisk syntax). chrome_input = `<html><body><!--StartFragment--><span class="user-group-mention user-mention-me" data-user-group-id="35" style="padding: 0px 3px; border-radius: 3px; white-space: nowrap; font-size: 0.95em; cursor: pointer; color: rgb(112, 203, 210); background-color: rgba(49, 150, 155, 0.2); font-weight: 600; font-family: "Source Sans 3 VF", sans-serif; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span class="mention-content-wrapper" style="font-size: 1.0526em;">@announce</span></span><span style="color: rgb(222, 222, 222); font-family: "Source Sans 3 VF", sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(31, 40, 40); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; display: inline !important; float: none;"> group ping</span><!--EndFragment--></body></html>`; assert.equal(compose_paste.paste_handler_converter(chrome_input), "@*announce* group ping"); // Silent group mention. chrome_input = `<html><body><!--StartFragment--><span class="user-group-mention silent" data-user-group-id="24" style="padding: 0px 3px; border-radius: 3px; white-space: nowrap; font-size: 0.95em; cursor: pointer; color: rgba(255, 255, 255, 0.8); background-color: rgba(49, 150, 155, 0.3); font-family: "Source Sans 3 VF", sans-serif; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span class="mention-content-wrapper" style="font-size: 1.0526em;">hamletcharacters</span></span><span style="color: rgb(222, 222, 222); font-family: "Source Sans 3 VF", sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(31, 40, 40); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; display: inline !important; float: none;">, silent group</span><!--EndFragment--></body></html>`; assert.equal( compose_paste.paste_handler_converter(chrome_input), "@_*hamletcharacters*, silent group", ); // Non-silent topic mention. chrome_input = `<html><body><!--StartFragment--><span class="topic-mention user-mention-me" style="padding: 0px 3px; border-radius: 3px; white-space: nowrap; font-size: 0.95em; color: rgb(112, 203, 210); background-color: rgba(49, 150, 155, 0.2); font-weight: 600; font-family: "Source Sans 3 VF", sans-serif; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span class="mention-content-wrapper" style="font-size: 1.0526em;">@topic</span></span><span style="color: rgb(222, 222, 222); font-family: "Source Sans 3 VF", sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(31, 40, 40); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; display: inline !important; float: none;"> topic test</span><!--EndFragment--></body></html>`; assert.equal(compose_paste.paste_handler_converter(chrome_input), "@**topic** topic test"); // Silent topic mention. chrome_input = `<html><body><!--StartFragment--><span class="topic-mention silent" style="padding: 0px 3px; border-radius: 3px; white-space: nowrap; font-size: 0.95em; color: rgba(255, 255, 255, 0.8); background-color: rgba(49, 150, 155, 0.2); font-family: "Source Sans 3 VF", sans-serif; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span class="mention-content-wrapper" style="font-size: 1.0526em;">topic</span></span><span style="color: rgb(222, 222, 222); font-family: "Source Sans 3 VF", sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(34, 34, 34); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; display: inline !important; float: none;"> silent topic</span><!--EndFragment--></body></html>`; assert.equal(compose_paste.paste_handler_converter(chrome_input), "@_**topic** silent topic"); // Non-silent channel wildcard mention (no |user_id suffix). chrome_input = `<html><body><!--StartFragment--><span class="user-mention channel-wildcard-mention user-mention-me" data-user-id="*" style="padding: 0px 3px; border-radius: 3px; white-space: nowrap; font-size: 0.95em; color: rgb(112, 203, 210); background-color: rgba(49, 150, 155, 0.2); cursor: pointer; font-weight: 600; font-family: "Source Sans 3 VF", sans-serif; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span class="mention-content-wrapper" style="font-size: 1.0526em;">@all</span></span><span style="color: rgb(222, 222, 222); font-family: "Source Sans 3 VF", sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(31, 40, 40); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; display: inline !important; float: none;"> wildcard</span><!--EndFragment--></body></html>`; assert.equal(compose_paste.paste_handler_converter(chrome_input), "@**all** wildcard"); // Silent channel wildcard mention. chrome_input = `<html><body><!--StartFragment--><span class="user-mention channel-wildcard-mention silent user-mention-me" data-user-id="*" style="padding: 0px 3px; border-radius: 3px; white-space: nowrap; font-size: 0.95em; color: rgb(112, 203, 210); background-color: rgba(49, 150, 155, 0.2); cursor: pointer; font-weight: 600; font-family: "Source Sans 3 VF", sans-serif; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span class="mention-content-wrapper" style="font-size: 1.0526em;">all</span></span><span style="color: rgb(222, 222, 222); font-family: "Source Sans 3 VF", sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(31, 40, 40); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; display: inline !important; float: none;"> silently</span><!--EndFragment--></body></html>`; assert.equal(compose_paste.paste_handler_converter(chrome_input), "@_**all** silently"); });