cynarAI 6c7eed7580 Initial clean evOo-local from f00577b
Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline
(Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/*
packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
2026-09-20 17:15:16 +02:00

50 lines
1.3 KiB
TypeScript

export type ChatMarkdownProps = {
children: string;
streaming?: boolean;
};
const protocolPattern = /^([a-z][a-z\d+.-]*):/i;
const safeProtocols = new Set(["http", "https", "mailto", "tel"]);
export function sanitizeMarkdownUrl(url: string, allowRelative = false): string | undefined {
const value = url.trim();
const protocol = value.match(protocolPattern)?.[1]?.toLowerCase();
if (protocol) return safeProtocols.has(protocol) ? value : undefined;
if (
allowRelative &&
(value.startsWith("/") ||
value.startsWith("./") ||
value.startsWith("../") ||
value.startsWith("#"))
) {
return value;
}
return undefined;
}
export function closeUnterminatedFence(markdown: string): string {
let openFence: { marker: "`" | "~"; length: number } | undefined;
for (const line of markdown.split("\n")) {
const match = line.match(/^ {0,3}(`{3,}|~{3,})(.*)$/);
if (!match?.[1]) continue;
const marker = match[1][0] as "`" | "~";
if (!openFence) {
openFence = { marker, length: match[1].length };
continue;
}
if (
marker === openFence.marker &&
match[1].length >= openFence.length &&
(match[2] ?? "").trim() === ""
) {
openFence = undefined;
}
}
return openFence ? `${markdown}\n${openFence.marker.repeat(openFence.length)}` : markdown;
}