atagen

malice

public
Star0
Watch0
c122e088bece1663d9f48cf1a0162c64d2817748
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git
web/src/lib/inline.tsTypescript52 lines1.8 KB
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
}