8fa1f6c250
- 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.
48 lines
1.4 KiB
TypeScript
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("<", "<")
|
|
.replaceAll(">", ">")
|
|
.replaceAll('"', """)
|
|
.replaceAll("'", "'");
|
|
}
|
|
|
|
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 }} />;
|
|
});
|