Files
oh-my-pi/packages/collab-web/src/components/transcript/Markdown.tsx
T
can1357 8fa1f6c250 feat: added shared collab wire protocol and web guest collaboration client
- Added @oh-my-pi/pi-wire and reworked collab protocol types into shared contracts.
- Added wire-compatibility guards in coding-agent host to block unsupported events.
- Added standalone collab-web package with guest UI, mock-host tooling, and local relay.
- Added secure room-link validation, WebCrypto framing, and safer socket routing.
2026-06-12 11:54:41 +02:00

48 lines
1.4 KiB
TypeScript

import { Marked } from "marked";
import type { ReactNode } from "react";
import { memo, useMemo } from "react";
function escapeHtml(s: string): string {
return s
.replaceAll("&", "&")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}
function safeHref(href: string): string | null {
const trimmed = href.trim();
if (/^(?:https?:|mailto:)/i.test(trimmed)) return trimmed;
if (/^[a-z][a-z0-9+.-]*:/i.test(trimmed)) return null; // unknown scheme (javascript:, data:, …)
return trimmed; // relative / fragment
}
const md = new Marked({
gfm: true,
renderer: {
// Raw HTML tokens (block + inline both arrive here) are escaped, never emitted.
html({ text }) {
return escapeHtml(text);
},
link({ href, title, tokens }) {
const inner = this.parser.parseInline(tokens);
const url = safeHref(href);
if (url === null) return inner;
const titleAttr = title ? ` title="${escapeHtml(title)}"` : "";
return `<a href="${escapeHtml(url)}"${titleAttr} target="_blank" rel="noopener">${inner}</a>`;
},
},
});
export const Markdown = memo(function Markdown({ text }: { text: string }): ReactNode {
const html = useMemo(() => {
try {
return md.parse(text, { async: false });
} catch {
return escapeHtml(text);
}
}, [text]);
return <div className="tr-md" dangerouslySetInnerHTML={{ __html: html }} />;
});