import { parseInlineMD } from '../plugins/markdown'; import { sanitizeText } from './sanitize'; /** * Room topic markdown → `m.room.topic` content (#262). * * Uses the composer's inline markdown parser (the same rules as messages: links, * bare URLs and `"` are handled), with one topic-specific guard: an `_` between * two letters/digits is literal, as in CommonMark. Topics are full of * `#room_alias:server`, `@user_name:server` and snake_case paths, which the * shared parser would otherwise turn into italics and, worse, drop from the * plain `topic` that every other client shows. */ // Spans the guard must not touch: inline code, link targets, bare URLs. const PROTECTED = /(`[^`\n]*`|\]\([^)\n]*\)|https?:\/\/[^\s<>"]+)/; const INTRAWORD_UNDERSCORES = /(?<=[\p{L}\p{N}])_+(?=[\p{L}\p{N}])/gu; const escapeIntrawordUnderscores = (line: string): string => line .split(PROTECTED) .map((part, i) => // split() with one capture group puts the protected spans at odd indexes. i % 2 === 1 ? part : part.replace(INTRAWORD_UNDERSCORES, (m) => m.replace(/_/g, '\\_')), ) .join(''); const ENTITIES: Record = { '&': '&', '<': '<', '>': '>', '"': '"', ''': "'", }; const decodeEntities = (s: string): string => s.replace(/&(?:amp|lt|gt|quot|#39);/g, (e) => ENTITIES[e]); /** Readable plain text for `topic`: markers dropped, links kept as "text (url)". */ const htmlToPlain = (html: string): string => decodeEntities( html .replace(/]*\bhref="([^"]*)"[^>]*>(.*?)<\/a>/g, (_m, href: string, label: string) => { const text = label.replace(/<[^>]+>/g, ''); return text === href ? href : `${text} (${href})`; }) .replace(/
/g, '\n') .replace(/<[^>]+>/g, ''), ); export type TopicContent = | { topic: string } | { topic: string; format: 'org.matrix.custom.html'; formatted_body: string }; export function buildTopicContent(text: string): TopicContent { const lines = text.split('\n'); const html = lines .map((line) => parseInlineMD(sanitizeText(escapeIntrawordUnderscores(line)))) .join('
'); const plainHtml = lines.map(sanitizeText).join('
'); // No markdown at all: send the text as typed, without a formatted body. if (html === plainHtml) return { topic: text }; return { topic: htmlToPlain(html), format: 'org.matrix.custom.html', formatted_body: html }; }