feat(web): add safe rich text formatting for message rendering

This commit is contained in:
2026-03-08 09:56:37 +03:00
parent 7c4a5f990d
commit cc70394960
3 changed files with 35 additions and 2 deletions

View File

@@ -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 {

View File

@@ -0,0 +1,32 @@
function escapeHtml(input: string): string {
return input
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
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;
}

View File

@@ -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"}