import type { AgentMentionInfo } from '../types'; import { buildAgentHref, buildSkillHref } from '@/lib/messages/inlineMessageLinks'; export const SKILL_TOKEN_PATTERN = /(^|\s)\/([a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?)/g; const FENCED_CODE_SEGMENT_PATTERN = /(```[\s\S]*?```|~~~[\s\S]*?~~~)/g; const escapeHtml = (text: string): string => { return text .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); }; const mapNonFencedSegments = (markdown: string, mapSegment: (segment: string) => string): string => { return markdown .split(FENCED_CODE_SEGMENT_PATTERN) .map((segment, index) => (index % 2 === 1 ? segment : mapSegment(segment))) .join(''); }; // In Markdown a single "\n" is a soft break (rendered as a space). Users type plain // text where each newline is meant literally, so convert soft breaks into hard breaks // (two trailing spaces) outside of fenced code blocks, where newlines are already literal. const applyHardLineBreaks = (markdown: string): string => { return mapNonFencedSegments(markdown, (segment) => segment.replace(/ *\n/g, ' \n')); }; export const prepareUserMarkdownContent = ({ textContent, agentMention, skillNames, }: { textContent: string; agentMention?: AgentMentionInfo; skillNames: ReadonlySet; }): string => { let content = mapNonFencedSegments(textContent, escapeHtml); // Insert agent mention links with an internal href so markdown renders them as mentions, not external links. if (agentMention?.token && content.includes(agentMention.token)) { const mentionMarkdown = `[${agentMention.token}](${buildAgentHref(agentMention.name)})`; content = content.replace(agentMention.token, mentionMarkdown); } content = content.replace(SKILL_TOKEN_PATTERN, (match, prefix: string, skillName: string) => { if (!skillNames.has(skillName)) return match; return `${prefix}[/${skillName}](${buildSkillHref(skillName)})`; }); // Preserve user newlines (markdown soft breaks would otherwise collapse to spaces) return applyHardLineBreaks(content); };