feat(web): add safe rich text formatting for message rendering
This commit is contained in:
@@ -5,6 +5,7 @@ import type { Message, MessageReaction } from "../chat/types";
|
||||
import { useAuthStore } from "../store/authStore";
|
||||
import { useChatStore } from "../store/chatStore";
|
||||
import { formatTime } from "../utils/format";
|
||||
import { formatMessageHtml } from "../utils/formatMessage";
|
||||
|
||||
type ContextMenuState = {
|
||||
x: number;
|
||||
@@ -555,7 +556,7 @@ function renderContent(messageType: string, text: string | null) {
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return <p className="whitespace-pre-wrap break-words">{text}</p>;
|
||||
return <p className="whitespace-pre-wrap break-words" dangerouslySetInnerHTML={{ __html: formatMessageHtml(text) }} />;
|
||||
}
|
||||
|
||||
function renderStatus(status: string | undefined): string {
|
||||
|
||||
32
web/src/utils/formatMessage.tsx
Normal file
32
web/src/utils/formatMessage.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
function escapeHtml(input: string): string {
|
||||
return input
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
function sanitizeHref(input: string): string {
|
||||
const normalized = input.trim();
|
||||
if (/^https?:\/\//i.test(normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
return "#";
|
||||
}
|
||||
|
||||
export function formatMessageHtml(text: string): string {
|
||||
let html = escapeHtml(text);
|
||||
|
||||
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_m, label: string, href: string) => {
|
||||
const safeHref = sanitizeHref(href);
|
||||
return `<a href="${safeHref}" target="_blank" rel="noreferrer" class="underline text-sky-300">${label}</a>`;
|
||||
});
|
||||
html = html.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
||||
html = html.replace(/\*([^*]+)\*/g, "<em>$1</em>");
|
||||
html = html.replace(/__([^_]+)__/g, "<u>$1</u>");
|
||||
html = html.replace(/`([^`]+)`/g, "<code class=\"rounded bg-slate-700/60 px-1 py-0.5 text-[12px]\">$1</code>");
|
||||
html = html.replace(/\|\|([^|]+)\|\|/g, "<span class=\"rounded bg-slate-700/80 px-1 text-transparent hover:text-inherit\">$1</span>");
|
||||
html = html.replace(/\n/g, "<br/>");
|
||||
return html;
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/api/auth.ts","./src/api/chats.ts","./src/api/http.ts","./src/api/notifications.ts","./src/api/search.ts","./src/api/users.ts","./src/app/app.tsx","./src/chat/types.ts","./src/components/apperrorboundary.tsx","./src/components/authpanel.tsx","./src/components/chatinfopanel.tsx","./src/components/chatlist.tsx","./src/components/messagecomposer.tsx","./src/components/messagelist.tsx","./src/components/newchatpanel.tsx","./src/hooks/userealtime.ts","./src/pages/authpage.tsx","./src/pages/chatspage.tsx","./src/store/authstore.ts","./src/store/chatstore.ts","./src/utils/format.ts","./src/utils/ws.ts"],"version":"5.9.2"}
|
||||
{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/api/auth.ts","./src/api/chats.ts","./src/api/http.ts","./src/api/notifications.ts","./src/api/search.ts","./src/api/users.ts","./src/app/app.tsx","./src/chat/types.ts","./src/components/apperrorboundary.tsx","./src/components/authpanel.tsx","./src/components/chatinfopanel.tsx","./src/components/chatlist.tsx","./src/components/messagecomposer.tsx","./src/components/messagelist.tsx","./src/components/newchatpanel.tsx","./src/hooks/userealtime.ts","./src/pages/authpage.tsx","./src/pages/chatspage.tsx","./src/store/authstore.ts","./src/store/chatstore.ts","./src/utils/format.ts","./src/utils/formatmessage.tsx","./src/utils/ws.ts"],"version":"5.9.2"}
|
||||
Reference in New Issue
Block a user