| 1 | import type { Mention } from './mentions'; |
| 2 | import { linkSpans } from './urls'; |
| 3 | |
| 4 | export type InlineSegment = |
| 5 | | { kind: 'text'; text: string } |
| 6 | | { kind: 'mention'; text: string; user_id: string } |
| 7 | | { kind: 'link'; text: string; href: string }; |
| 8 | |
| 9 | interface Span { |
| 10 | start: number; |
| 11 | end: number; |
| 12 | segment: InlineSegment; |
| 13 | } |
| 14 | |
| 15 | const WORD_CHAR = /[\p{L}\p{N}_]/u; |
| 16 | |
| 17 | function escapeRegExp(text: string): string { |
| 18 | return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); |
| 19 | } |
| 20 | |
| 21 | function mentionSpans(text: string, mentions: Mention[], taken: Span[]): Span[] { |
| 22 | const byText = new Map(mentions.filter((m) => m.text).map((m) => [m.text, m.user_id])); |
| 23 | if (byText.size === 0) return []; |
| 24 | const names = [...byText.keys()].sort((a, b) => b.length - a.length); |
| 25 | const out: Span[] = []; |
| 26 | for (const m of text.matchAll(new RegExp(names.map(escapeRegExp).join('|'), 'g'))) { |
| 27 | const start = m.index; |
| 28 | const end = start + m[0].length; |
| 29 | if (WORD_CHAR.test(text[start - 1] ?? '') || WORD_CHAR.test(text[end] ?? '')) continue; |
| 30 | if (taken.some((s) => start < s.end && end > s.start)) continue; |
| 31 | out.push({ start, end, segment: { kind: 'mention', text: m[0], user_id: byText.get(m[0])! } }); |
| 32 | } |
| 33 | return out; |
| 34 | } |
| 35 | |
| 36 | export function segmentInline(text: string, mentions: Mention[]): InlineSegment[] { |
| 37 | const links: Span[] = linkSpans(text).map((l) => ({ |
| 38 | start: l.start, |
| 39 | end: l.end, |
| 40 | segment: { kind: 'link', text: text.slice(l.start, l.end), href: l.href } |
| 41 | })); |
| 42 | const spans = [...links, ...mentionSpans(text, mentions, links)].sort((a, b) => a.start - b.start); |
| 43 | const out: InlineSegment[] = []; |
| 44 | let last = 0; |
| 45 | for (const s of spans) { |
| 46 | if (s.start > last) out.push({ kind: 'text', text: text.slice(last, s.start) }); |
| 47 | out.push(s.segment); |
| 48 | last = s.end; |
| 49 | } |
| 50 | if (last < text.length) out.push({ kind: 'text', text: text.slice(last) }); |
| 51 | return out; |
| 52 | } |