mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-11-11 18:30:36 +08:00
Fix emoji in markdown code blocks
Emoji in code blocks were missing from both the rendered code and the copied text. twemoji replaces them with images, but highlighting and the copy button work from the block's text content, which leaves images out. We now turn the images back into text first.
This commit is contained in:
@@ -35,6 +35,7 @@
|
||||
import {
|
||||
isUrl,
|
||||
twemoji,
|
||||
untwemoji,
|
||||
scrollIntoView,
|
||||
canonicalize,
|
||||
canonicalizeGithubImageUrl,
|
||||
@@ -248,6 +249,7 @@
|
||||
for (const node of nodes) {
|
||||
const preElement = node.parentElement as HTMLElement;
|
||||
const copyButton = document.createElement("radicle-clipboard");
|
||||
untwemoji(node);
|
||||
copyButton.setAttribute("text", node.textContent || "");
|
||||
// Create a wrapper around the pre element,
|
||||
// so we can position the copy button that works even when scrolling horizontally.
|
||||
|
||||
@@ -315,6 +315,12 @@ export function twemoji(
|
||||
});
|
||||
}
|
||||
|
||||
export function untwemoji(node: Element) {
|
||||
for (const img of node.querySelectorAll("img.txt-emoji")) {
|
||||
img.replaceWith(img.getAttribute("alt") ?? "");
|
||||
}
|
||||
}
|
||||
|
||||
// Formats COBs Object Ids
|
||||
export function formatObjectId(id: string): string {
|
||||
return id.substring(0, 7);
|
||||
|
||||
@@ -567,6 +567,35 @@ test.describe("browser error handling", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("emoji in markdown code blocks", async ({ page }) => {
|
||||
await page.goto(`${markdownUrl}/tree/main/markdown-code.md`);
|
||||
|
||||
const highlighted = page
|
||||
.locator("div.pre-wrapper")
|
||||
.filter({ hasText: "alert" });
|
||||
const highlightedText =
|
||||
'alert("test"); // ❌\nconfirm("test"); // ❌✅\nprompt("test"); // 👩💻\n';
|
||||
await expect(highlighted.locator("code .pl-c")).toHaveText([
|
||||
"// ❌",
|
||||
"// ❌✅",
|
||||
"// 👩💻",
|
||||
]);
|
||||
await expect(highlighted.locator("code")).toHaveText(highlightedText);
|
||||
await expect(highlighted.locator("img")).toHaveCount(0);
|
||||
await expect(highlighted.locator("radicle-clipboard")).toHaveAttribute(
|
||||
"text",
|
||||
highlightedText,
|
||||
);
|
||||
|
||||
const plain = page.locator("div.pre-wrapper").filter({ hasText: "plain:" });
|
||||
await expect(plain.locator("code")).toHaveText("plain: ❌ ✅\n");
|
||||
await expect(plain.locator("img")).toHaveCount(0);
|
||||
await expect(plain.locator("radicle-clipboard")).toHaveAttribute(
|
||||
"text",
|
||||
"plain: ❌ ✅\n",
|
||||
);
|
||||
});
|
||||
|
||||
test("external markdown link", async ({ context, page }) => {
|
||||
await context.route("https://example.com/**", route => {
|
||||
return route.fulfill({ body: "hello", contentType: "text/plain" });
|
||||
|
||||
@@ -559,6 +559,21 @@ export async function createMarkdownFixture(peer: RadiclePeer) {
|
||||
await Fs.cp(Path.join(tmpDir, "repos", "markdown"), repoFolder, {
|
||||
recursive: true,
|
||||
});
|
||||
await Fs.writeFile(
|
||||
Path.join(repoFolder, "markdown-code.md"),
|
||||
[
|
||||
"```javascript",
|
||||
'alert("test"); // ❌',
|
||||
'confirm("test"); // ❌✅',
|
||||
'prompt("test"); // 👩💻',
|
||||
"```",
|
||||
"",
|
||||
"```",
|
||||
"plain: ❌ ✅",
|
||||
"```",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
await peer.git(["add", "."], { cwd: repoFolder });
|
||||
const commitMessage = `Add Markdown cheat sheet
|
||||
|
||||
@@ -375,3 +375,57 @@ describe("safeDecodeURIComponent", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("untwemoji", () => {
|
||||
function code(text: string) {
|
||||
const node = document.createElement("code");
|
||||
node.textContent = text;
|
||||
utils.twemoji(node);
|
||||
return node;
|
||||
}
|
||||
|
||||
test("restores emoji turned into images by twemoji", () => {
|
||||
const text =
|
||||
'alert("test"); // ❌\nconfirm("test"); // ❌\nprompt("test"); // ❌\n';
|
||||
const node = code(text);
|
||||
expect(node.querySelectorAll("img.txt-emoji")).toHaveLength(3);
|
||||
|
||||
utils.untwemoji(node);
|
||||
expect(node.querySelector("img")).toBeNull();
|
||||
expect(node.textContent).toEqual(text);
|
||||
});
|
||||
|
||||
test("restores adjacent and multi-codepoint emoji", () => {
|
||||
const text = "❌❌✅ 👩💻 👍🏽 🇱🇻";
|
||||
const node = code(text);
|
||||
utils.untwemoji(node);
|
||||
expect(node.querySelector("img")).toBeNull();
|
||||
expect(node.textContent).toEqual(text);
|
||||
});
|
||||
|
||||
test("leaves images that are not emoji alone", () => {
|
||||
const node = document.createElement("code");
|
||||
node.innerHTML = '<img alt="logo" src="logo.png">';
|
||||
utils.untwemoji(node);
|
||||
expect(node.innerHTML).toEqual('<img alt="logo" src="logo.png">');
|
||||
});
|
||||
|
||||
test("inserts alt text as text, never as markup", () => {
|
||||
const node = document.createElement("code");
|
||||
const img = document.createElement("img");
|
||||
img.className = "txt-emoji";
|
||||
img.alt = "<img src=x onerror=alert(1)>";
|
||||
node.append(img);
|
||||
|
||||
utils.untwemoji(node);
|
||||
expect(node.childElementCount).toEqual(0);
|
||||
expect(node.textContent).toEqual("<img src=x onerror=alert(1)>");
|
||||
});
|
||||
|
||||
test("drops emoji images without alt text", () => {
|
||||
const node = document.createElement("code");
|
||||
node.innerHTML = 'a<img class="txt-emoji" src="/twemoji/274c.svg">b';
|
||||
utils.untwemoji(node);
|
||||
expect(node.innerHTML).toEqual("ab");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user