mirror of https://github.com/zulip/zulip.git
turndown: Improve pasting experience, focused on pasting Zulip messages.
Turned off and tested escaping with `/` (for now). Added support + tests for: - headings. - strikethrough. - nested lists. - code blocks. Improved handling of: - links (custom and raw, ignored when wrapping a single image). - images (now pasted in Zulip's link like syntax). - custom emojis. - LaTeX (no garbage symbols, unformatted plain text is pasted) Added tests for emojis. Known concerns: - External images aren't handled anymore by upload.js -- is this a bug? - Tables lose their formatting on paste.
This commit is contained in:
parent
7c10775e96
commit
a01d670fb2
|
@ -294,29 +294,165 @@ function get_end_tr_from_endc($endc) {
|
|||
return $endc.parents(".selectable_row").first();
|
||||
}
|
||||
|
||||
function deduplicate_newlines(attribute) {
|
||||
// We replace any occurrences of one or more consecutive newlines followed by
|
||||
// zero or more whitespace characters with a single newline character.
|
||||
return attribute ? attribute.replaceAll(/(\n+\s*)+/g, "\n") : "";
|
||||
}
|
||||
|
||||
function image_to_zulip_markdown(_content, node) {
|
||||
if (node.nodeName === "IMG" && node.classList.contains("emoji") && node.hasAttribute("alt")) {
|
||||
// For Zulip's custom emoji
|
||||
return node.getAttribute("alt");
|
||||
}
|
||||
const src = node.getAttribute("src") || node.getAttribute("href") || "";
|
||||
const title = deduplicate_newlines(node.getAttribute("title")) || "";
|
||||
// Using Zulip's link like syntax for images
|
||||
return src ? "[" + title + "](" + src + ")" : node.getAttribute("alt") || "";
|
||||
}
|
||||
|
||||
export function paste_handler_converter(paste_html) {
|
||||
const turndownService = new TurndownService();
|
||||
turndownService.addRule("headings", {
|
||||
filter: ["h1", "h2", "h3", "h4", "h5", "h6"],
|
||||
// turning off escaping (for now) to remove extra `/`
|
||||
TurndownService.prototype.escape = (string) => string;
|
||||
|
||||
const turndownService = new TurndownService({
|
||||
emDelimiter: "*",
|
||||
codeBlockStyle: "fenced",
|
||||
headingStyle: "atx",
|
||||
});
|
||||
turndownService.addRule("strikethrough", {
|
||||
filter: ["del", "s", "strike"],
|
||||
replacement(content) {
|
||||
return content;
|
||||
return "~~" + content + "~~";
|
||||
},
|
||||
});
|
||||
turndownService.addRule("emphasis", {
|
||||
filter: ["em", "i"],
|
||||
replacement(content) {
|
||||
return "*" + content + "*";
|
||||
},
|
||||
});
|
||||
// Checks for raw links without custom text or title.
|
||||
turndownService.addRule("links", {
|
||||
filter(node) {
|
||||
filter: ["a"],
|
||||
replacement(content, node) {
|
||||
if (node.href === content) {
|
||||
// Checks for raw links without custom text.
|
||||
return content;
|
||||
}
|
||||
if (node.childNodes.length === 1 && node.firstChild.nodeName === "IMG") {
|
||||
// ignore link's url if it only has an image
|
||||
return content;
|
||||
}
|
||||
return "[" + content + "](" + node.href + ")";
|
||||
},
|
||||
});
|
||||
turndownService.addRule("listItem", {
|
||||
// We override the original upstream implementation of this rule
|
||||
// to have a custom indent of 2 spaces for list items, instead of
|
||||
// the default 4 spaces. Everything else is the same as upstream.
|
||||
filter: "li",
|
||||
replacement(content, node) {
|
||||
content = content
|
||||
.replace(/^\n+/, "") // remove leading newlines
|
||||
.replace(/\n+$/, "\n") // replace trailing newlines with just a single one
|
||||
.replaceAll(/\n/gm, "\n "); // custom 2 space indent
|
||||
let prefix = "* ";
|
||||
const parent = node.parentNode;
|
||||
if (parent.nodeName === "OL") {
|
||||
const start = parent.getAttribute("start");
|
||||
const index = Array.prototype.indexOf.call(parent.children, node);
|
||||
prefix = (start ? Number(start) + index : index + 1) + ". ";
|
||||
}
|
||||
return prefix + content + (node.nextSibling && !/\n$/.test(content) ? "\n" : "");
|
||||
},
|
||||
});
|
||||
turndownService.addRule("zulipCodeBlock", {
|
||||
// We create a new rule to exclusively handle code blocks in Zulip messages since
|
||||
// the `fencedCodeBlock` rule in upstream won't work for them. The reason is that
|
||||
// `fencedCodeBlock` only works for `pre` elements that have `code` elements as
|
||||
// their 1st child, while Zulip code blocks have an empty span as the 1st child
|
||||
// of the `pre` element, and then the `code` element. This new rule is a variation
|
||||
// of upstream's `fencedCodeBlock` rule.
|
||||
|
||||
// We modify the filter of upstream's `fencedCodeBlock` rule to only apply to
|
||||
// Zulip code blocks with the Zulip specific class of `zulip-code-block`.
|
||||
filter(node, options) {
|
||||
return (
|
||||
node.nodeName === "A" && node.href === node.innerHTML && node.href === node.title
|
||||
options.codeBlockStyle === "fenced" &&
|
||||
node.nodeName === "CODE" &&
|
||||
node.parentElement?.nodeName === "PRE" &&
|
||||
node.parentElement.parentElement?.classList.contains("zulip-code-block")
|
||||
);
|
||||
},
|
||||
replacement(content) {
|
||||
return content;
|
||||
|
||||
// We modify the replacement of upstream's `fencedCodeBlock` rule only slightly
|
||||
// to extract and add the language of the code block (if any) to the fence.
|
||||
replacement(content, node, options) {
|
||||
const language = node.closest(".codehilite")?.dataset?.codeLanguage || "";
|
||||
|
||||
const fenceChar = options.fence.charAt(0);
|
||||
let fenceSize = 3;
|
||||
const fenceInCodeRegex = new RegExp("^" + fenceChar + "{3,}", "gm");
|
||||
|
||||
let match;
|
||||
while ((match = fenceInCodeRegex.exec(content))) {
|
||||
if (match[0].length >= fenceSize) {
|
||||
fenceSize = match[0].length + 1;
|
||||
}
|
||||
}
|
||||
|
||||
const fence = fenceChar.repeat(fenceSize);
|
||||
|
||||
return (
|
||||
"\n\n" +
|
||||
fence +
|
||||
language +
|
||||
"\n" +
|
||||
content.replace(/\n$/, "") +
|
||||
"\n" +
|
||||
fence +
|
||||
"\n\n"
|
||||
);
|
||||
},
|
||||
});
|
||||
turndownService.addRule("zulipImagePreview", {
|
||||
filter(node) {
|
||||
// select image previews in Zulip messages
|
||||
return (
|
||||
node.classList.contains("message_inline_image") && node.firstChild.nodeName === "A"
|
||||
);
|
||||
},
|
||||
|
||||
replacement(content, node) {
|
||||
// We parse the copied html to then check if the generating link (which, if
|
||||
// present, always comes before the preview in the copied html) is also there.
|
||||
|
||||
// If the preview has an aria-label, it means it does have a named link in the
|
||||
// message, and if the 1st element with the same image link in the copied html
|
||||
// does not have the `message_inline_image` class, it means it is the generating
|
||||
// link, and not the preview, meaning the generating link is copied as well.
|
||||
const copied_html = new DOMParser().parseFromString(paste_html, "text/html");
|
||||
if (
|
||||
node.firstChild.hasAttribute("aria-label") &&
|
||||
!copied_html
|
||||
.querySelector("a[href='" + node.firstChild.getAttribute("href") + "']")
|
||||
?.parentNode?.classList.contains("message_inline_image")
|
||||
) {
|
||||
// We skip previews which have their generating link copied too, to avoid
|
||||
// double pasting the same link.
|
||||
return "";
|
||||
}
|
||||
return image_to_zulip_markdown(content, node.firstChild);
|
||||
},
|
||||
});
|
||||
turndownService.addRule("images", {
|
||||
filter: "img",
|
||||
|
||||
replacement: image_to_zulip_markdown,
|
||||
});
|
||||
turndownService.addRule("math", {
|
||||
// We don't have a way to get the original LaTeX code from the rendered
|
||||
// `math` so we drop it to avoid pasting gibberish.
|
||||
// In the future, we could have a data-original-latex feature in Zulip HTML
|
||||
// if we wanted to paste the original LaTeX for Zulip messages.
|
||||
filter: "math",
|
||||
|
||||
replacement() {
|
||||
return "";
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -390,15 +526,9 @@ export function paste_handler(event) {
|
|||
}
|
||||
|
||||
if (paste_html && page_params.development_environment) {
|
||||
const text = paste_handler_converter(paste_html);
|
||||
const mdImageRegex = /^!\[.*]\(.*\)$/;
|
||||
if (mdImageRegex.test(text)) {
|
||||
// This block catches cases where we are pasting an
|
||||
// image into Zulip, which is handled by upload.js.
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const text = paste_handler_converter(paste_html);
|
||||
compose_ui.insert_syntax_and_focus(text);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,12 @@ const copy_and_paste = zrequire("copy_and_paste");
|
|||
|
||||
run_test("paste_handler_converter", () => {
|
||||
page_params.development_environment = true;
|
||||
|
||||
/*
|
||||
Pasting from another Zulip message
|
||||
*/
|
||||
|
||||
// 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(
|
||||
|
@ -17,10 +23,19 @@ run_test("paste_handler_converter", () => {
|
|||
" 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(copy_and_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 style="margin: 3px 0px; 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;">zulip code block in python</p><div class="codehilite zulip-code-block" data-code-language="Python" style="background-color: rgb(33, 45, 59); display: block !important; border: none !important; background-image: none !important; background-position: initial !important; background-size: initial !important; background-repeat: initial !important; background-attachment: initial !important; background-origin: initial !important; background-clip: initial !important; 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; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><pre style="padding: 5px 7px 3px; font-family: "Source Code Pro", monospace; font-size: 0.825em; color: rgb(163, 206, 255); border-radius: 4px; display: block; margin: 5px 0px; line-height: 1.4; word-break: break-all; overflow-wrap: normal; white-space: pre; background-color: rgb(29, 38, 48); border: 1px solid rgba(0, 0, 0, 0.15); direction: ltr; overflow-x: auto;"><span></span><code style="font-family: "Source Code Pro", monospace; font-size: inherit; unicode-bidi: embed; direction: ltr; color: rgb(163, 206, 255); white-space: inherit; padding: 0px; background-color: rgb(29, 38, 48); border: 0px rgba(0, 0, 0, 0.5); border-radius: 3px; overflow-x: scroll;"><span class="nb" style="color: rgb(239, 239, 143);">print</span><span class="p" style="color: rgb(65, 113, 113);">(</span><span class="s2" style="color: rgb(204, 147, 147);">"hello world"</span><span class="p" style="color: rgb(65, 113, 113);">)</span></code></pre></div></meta>`;
|
||||
assert.equal(
|
||||
copy_and_paste.paste_handler_converter(input),
|
||||
'zulip code block in python\n\n```Python\nprint("hello world")\n```',
|
||||
);
|
||||
|
||||
// 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(
|
||||
|
@ -28,6 +43,7 @@ run_test("paste_handler_converter", () => {
|
|||
"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(
|
||||
|
@ -35,29 +51,58 @@ run_test("paste_handler_converter", () => {
|
|||
"[Contributing guide](https://zulip.readthedocs.io/en/latest/contributing/contributing.html)",
|
||||
);
|
||||
|
||||
// Numbered list item
|
||||
input =
|
||||
'<meta http-equiv="content-type" content="text/html; charset=utf-8"><span style="color: hsl(0, 0%, 0%); font-family: "Helvetica Neue", "Segoe UI", Helvetica, Arial, sans-serif; font-size: 13px; 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;">1. text</span>';
|
||||
assert.equal(copy_and_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>';
|
||||
assert.equal(copy_and_paste.paste_handler_converter(input), "Zulip overview");
|
||||
assert.equal(copy_and_paste.paste_handler_converter(input), "# Zulip overview");
|
||||
|
||||
// Italic text
|
||||
input =
|
||||
'<meta http-equiv="content-type" content="text/html; charset=utf-8"><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(copy_and_paste.paste_handler_converter(input), "*This text is italic*");
|
||||
|
||||
// Strikethrough text
|
||||
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>';
|
||||
'<meta http-equiv="content-type" content="text/html; charset=utf-8"><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(copy_and_paste.paste_handler_converter(input), "~~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-1f642" 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/srv/zulip-npm-cache/287cb53c1a095fe79651f095d5d8d60f7060baa7/node_modules/emoji-datasource-google/img/google/sheets-256/64.png"); 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/srv/zulip-npm-cache/287cb53c1a095fe79651f095d5d8d60f7060baa7/node_modules/emoji-datasource-google/img/google/sheets-256/64.png"); 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(
|
||||
copy_and_paste.paste_handler_converter(input),
|
||||
"Test list:\n* Item 1\n* Item 2",
|
||||
"emojis: :smile: :family_man_woman_girl:",
|
||||
);
|
||||
|
||||
// Nested lists
|
||||
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>';
|
||||
'<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(
|
||||
copy_and_paste.paste_handler_converter(input),
|
||||
"Test list:\n* Item 1\n* Item 2",
|
||||
"* bulleted\n* nested\n * nested level 1\n * nested level 1 continue\n * nested level 2\n * nested level 2 continue",
|
||||
);
|
||||
|
||||
// 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(copy_and_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(copy_and_paste.paste_handler_converter(input), "Test list:\n* Item 1\n* Item 2");
|
||||
|
||||
// Pasting code from VS Code / Gmail
|
||||
input =
|
||||
'<meta http-equiv="content-type" content="text/html; charset=utf-8"><div style="color: #ffffff;background-color: #002451;font-family: Consolas, "Courier New", monospace;font-weight: normal;font-size: 14px;line-height: 19px;white-space: pre;"><div><span style="color: #ebbbff;">const</span><span style="color: #ffffff;"> </span><span style="color: #ff9da4;">compose_ui</span><span style="color: #ffffff;"> </span><span style="color: #99ffff;">=</span><span style="color: #ffffff;"> </span><span style="color: #bbdaff;">mock_esm</span><span style="color: #ffffff;">(</span><span style="color: #d1f1a9;">"../src/compose_ui"</span><span style="color: #ffffff;">);</span></div><div><span style="color: #bbdaff;">set_global</span><span style="color: #ffffff;">(</span><span style="color: #d1f1a9;">"document"</span><span style="color: #ffffff;">, </span><span style="color: #ff9da4;">document</span><span style="color: #ffffff;">);</span></div></div>';
|
||||
assert.equal(
|
||||
copy_and_paste.paste_handler_converter(input),
|
||||
'const compose_ui = mock_esm("../src/compose_ui");\n\nset_global("document", document);',
|
||||
);
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue