diff --git a/imports/client/components/ChatMessage.tsx b/imports/client/components/ChatMessage.tsx
index 8e25705e3..83c3f7f2c 100644
--- a/imports/client/components/ChatMessage.tsx
+++ b/imports/client/components/ChatMessage.tsx
@@ -1,4 +1,5 @@
/* eslint-disable react/no-array-index-key */
+import * as he from "he";
import { marked } from "marked";
import React from "react";
import styled from "styled-components";
@@ -38,6 +39,9 @@ const StyledCodeBlock = styled.code`
// Renders a markdown token to React components.
const MarkdownToken = ({ token }: { token: marked.Token }) => {
+ // NOTE: Marked's lexer encodes using HTML entities in the text; see:
+ // https://github.com/markedjs/marked/discussions/1737
+ // We need to decode the text since React will apply its own escaping.
if (token.type === "text") {
return {token.raw};
} else if (token.type === "space") {
@@ -48,8 +52,9 @@ const MarkdownToken = ({ token }: { token: marked.Token }) => {
const children = token.tokens.map((t, i) => (
));
- if (token.raw.length > token.text.length) {
- const trail = token.raw.substring(token.text.length);
+ const decodedText = he.decode(token.text);
+ if (token.raw.length > decodedText.length) {
+ const trail = token.raw.substring(decodedText.length);
if (trail.trim() === "") {
const syntheticSpace: marked.Tokens.Space = {
type: "space",
@@ -95,9 +100,11 @@ const MarkdownToken = ({ token }: { token: marked.Token }) => {
));
return {children};
} else if (token.type === "codespan") {
- return {token.text}
;
+ const decodedText = he.decode(token.text);
+ return {decodedText}
;
} else if (token.type === "code") {
- return {token.text};
+ const decodedText = he.decode(token.text);
+ return {decodedText};
} else {
// Unhandled token types: just return the raw string with pre-wrap.
// This covers things like bulleted or numbered lists, which we explicitly
diff --git a/package-lock.json b/package-lock.json
index be37886b6..927f7ad90 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7062,6 +7062,12 @@
"@types/node": "*"
}
},
+ "@types/he": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@types/he/-/he-1.2.3.tgz",
+ "integrity": "sha512-q67/qwlxblDzEDvzHhVkwc1gzVWxaNxeyHUBF4xElrvjL11O+Ytze+1fGpBHlr/H9myiBUaUXNnNPmBHxxfAcA==",
+ "dev": true
+ },
"@types/http-errors": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
@@ -15055,8 +15061,7 @@
"he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
- "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
- "dev": true
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="
},
"hoist-non-react-statics": {
"version": "3.3.2",
diff --git a/package.json b/package.json
index 96792c16e..717cc9f87 100644
--- a/package.json
+++ b/package.json
@@ -45,6 +45,7 @@
"form-data": "^4.0.1",
"gaxios": "^6.7.1",
"glob": "^8.1.0",
+ "he": "^1.2.0",
"http-proxy": "^1.18.1",
"ip-address": "^10.0.1",
"logfmt": "^1.4.0",
@@ -84,6 +85,7 @@
"@types/element-resize-detector": "^1.1.6",
"@types/express": "^4.17.21",
"@types/glob": "^8.1.0",
+ "@types/he": "^1.2.3",
"@types/http-proxy": "^1.17.15",
"@types/jsbn": "^1.2.33",
"@types/lodash": "^4.17.13",