atagen

malice

public
Star0
Watch0
705b1e1062c28d089cbd8a8c402d6fbf7b312a29
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git

Blame: web/src/lib/code.ts

cf23dbf4e77catagen2026-10-23 06:36 UTC1
import type { Mention } from './mentions';
cf23dbf4e77catagen2026-10-23 06:36 UTC2
df3c0f2b3da4atagen2026-10-22 13:07 UTC3
export type CodeSegment =
df3c0f2b3da4atagen2026-10-22 13:07 UTC4
  | { kind: 'text'; text: string }
df3c0f2b3da4atagen2026-10-22 13:07 UTC5
  | { kind: 'inline'; text: string }
df3c0f2b3da4atagen2026-10-22 13:07 UTC6
  | { kind: 'block'; text: string; lang: string | null };
df3c0f2b3da4atagen2026-10-22 13:07 UTC7
df3c0f2b3da4atagen2026-10-22 13:07 UTC8
const FENCE = /```([^\s`]*)[^\S\n]*\n?([\s\S]*?)\n?```/g;
df3c0f2b3da4atagen2026-10-22 13:07 UTC9
const INLINE = /`([^`\n]+)`/g;
df3c0f2b3da4atagen2026-10-22 13:07 UTC10
df3c0f2b3da4atagen2026-10-22 13:07 UTC11
function splitInline(text: string, out: CodeSegment[]): void {
df3c0f2b3da4atagen2026-10-22 13:07 UTC12
  let last = 0;
df3c0f2b3da4atagen2026-10-22 13:07 UTC13
  for (const m of text.matchAll(INLINE)) {
df3c0f2b3da4atagen2026-10-22 13:07 UTC14
    if (m.index > last) out.push({ kind: 'text', text: text.slice(last, m.index) });
df3c0f2b3da4atagen2026-10-22 13:07 UTC15
    out.push({ kind: 'inline', text: m[1] });
df3c0f2b3da4atagen2026-10-22 13:07 UTC16
    last = m.index + m[0].length;
df3c0f2b3da4atagen2026-10-22 13:07 UTC17
  }
df3c0f2b3da4atagen2026-10-22 13:07 UTC18
  if (last < text.length) out.push({ kind: 'text', text: text.slice(last) });
df3c0f2b3da4atagen2026-10-22 13:07 UTC19
}
df3c0f2b3da4atagen2026-10-22 13:07 UTC20
df3c0f2b3da4atagen2026-10-22 13:07 UTC21
export function parseCode(text: string): CodeSegment[] {
df3c0f2b3da4atagen2026-10-22 13:07 UTC22
  const out: CodeSegment[] = [];
df3c0f2b3da4atagen2026-10-22 13:07 UTC23
  if (!text.includes('`')) {
df3c0f2b3da4atagen2026-10-22 13:07 UTC24
    if (text) out.push({ kind: 'text', text });
df3c0f2b3da4atagen2026-10-22 13:07 UTC25
    return out;
df3c0f2b3da4atagen2026-10-22 13:07 UTC26
  }
df3c0f2b3da4atagen2026-10-22 13:07 UTC27
  let last = 0;
df3c0f2b3da4atagen2026-10-22 13:07 UTC28
  for (const m of text.matchAll(FENCE)) {
df3c0f2b3da4atagen2026-10-22 13:07 UTC29
    splitInline(text.slice(last, m.index), out);
df3c0f2b3da4atagen2026-10-22 13:07 UTC30
    out.push({ kind: 'block', text: m[2], lang: m[1] || null });
df3c0f2b3da4atagen2026-10-22 13:07 UTC31
    last = m.index + m[0].length;
df3c0f2b3da4atagen2026-10-22 13:07 UTC32
    if (text[last] === '\n') last += 1;
df3c0f2b3da4atagen2026-10-22 13:07 UTC33
  }
df3c0f2b3da4atagen2026-10-22 13:07 UTC34
  splitInline(text.slice(last), out);
df3c0f2b3da4atagen2026-10-22 13:07 UTC35
  return out;
df3c0f2b3da4atagen2026-10-22 13:07 UTC36
}
df3c0f2b3da4atagen2026-10-22 13:07 UTC37
df3c0f2b3da4atagen2026-10-22 13:07 UTC38
export function hasCode(segments: CodeSegment[]): boolean {
df3c0f2b3da4atagen2026-10-22 13:07 UTC39
  return segments.some((s) => s.kind !== 'text');
df3c0f2b3da4atagen2026-10-22 13:07 UTC40
}
df3c0f2b3da4atagen2026-10-22 13:07 UTC41
df3c0f2b3da4atagen2026-10-22 13:07 UTC42
function escapeHtml(text: string): string {
df3c0f2b3da4atagen2026-10-22 13:07 UTC43
  return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
df3c0f2b3da4atagen2026-10-22 13:07 UTC44
}
df3c0f2b3da4atagen2026-10-22 13:07 UTC45
cf23dbf4e77catagen2026-10-23 06:36 UTC46
function escapeRegExp(text: string): string {
cf23dbf4e77catagen2026-10-23 06:36 UTC47
  return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
cf23dbf4e77catagen2026-10-23 06:36 UTC48
}
cf23dbf4e77catagen2026-10-23 06:36 UTC49
cf23dbf4e77catagen2026-10-23 06:36 UTC50
function textToHtml(text: string, pattern: RegExp | null, byText: Map<string, string>): string {
cf23dbf4e77catagen2026-10-23 06:36 UTC51
  if (!pattern) return escapeHtml(text).replace(/\n/g, '<br>');
cf23dbf4e77catagen2026-10-23 06:36 UTC52
  let out = '';
cf23dbf4e77catagen2026-10-23 06:36 UTC53
  let last = 0;
cf23dbf4e77catagen2026-10-23 06:36 UTC54
  for (const m of text.matchAll(pattern)) {
cf23dbf4e77catagen2026-10-23 06:36 UTC55
    out += escapeHtml(text.slice(last, m.index)).replace(/\n/g, '<br>');
cf23dbf4e77catagen2026-10-23 06:36 UTC56
    const userId = byText.get(m[0]) ?? m[0];
cf23dbf4e77catagen2026-10-23 06:36 UTC57
    out += `<a href="https://matrix.to/#/${escapeHtml(userId)}">${escapeHtml(m[0])}</a>`;
cf23dbf4e77catagen2026-10-23 06:36 UTC58
    last = m.index + m[0].length;
cf23dbf4e77catagen2026-10-23 06:36 UTC59
  }
cf23dbf4e77catagen2026-10-23 06:36 UTC60
  return out + escapeHtml(text.slice(last)).replace(/\n/g, '<br>');
cf23dbf4e77catagen2026-10-23 06:36 UTC61
}
cf23dbf4e77catagen2026-10-23 06:36 UTC62
cf23dbf4e77catagen2026-10-23 06:36 UTC63
/** Matrix `formatted_body` for a plain body, or null when it has no code or mentions to format. */
cf23dbf4e77catagen2026-10-23 06:36 UTC64
export function codeToHtml(body: string, mentions: Mention[] = []): string | null {
df3c0f2b3da4atagen2026-10-22 13:07 UTC65
  const segments = parseCode(body);
cf23dbf4e77catagen2026-10-23 06:36 UTC66
  const byText = new Map(mentions.map((m) => [m.text, m.user_id]));
cf23dbf4e77catagen2026-10-23 06:36 UTC67
  const texts = [...byText.keys()].sort((a, b) => b.length - a.length);
cf23dbf4e77catagen2026-10-23 06:36 UTC68
  const pattern = texts.length > 0 ? new RegExp(texts.map(escapeRegExp).join('|'), 'g') : null;
cf23dbf4e77catagen2026-10-23 06:36 UTC69
  const linked = pattern !== null && segments.some((s) => s.kind === 'text' && new RegExp(pattern.source).test(s.text));
cf23dbf4e77catagen2026-10-23 06:36 UTC70
  if (!hasCode(segments) && !linked) return null;
df3c0f2b3da4atagen2026-10-22 13:07 UTC71
  return segments
df3c0f2b3da4atagen2026-10-22 13:07 UTC72
    .map((s) => {
df3c0f2b3da4atagen2026-10-22 13:07 UTC73
      if (s.kind === 'inline') return `<code>${escapeHtml(s.text)}</code>`;
df3c0f2b3da4atagen2026-10-22 13:07 UTC74
      if (s.kind === 'block') {
df3c0f2b3da4atagen2026-10-22 13:07 UTC75
        const cls = s.lang ? ` class="language-${escapeHtml(s.lang)}"` : '';
df3c0f2b3da4atagen2026-10-22 13:07 UTC76
        return `<pre><code${cls}>${escapeHtml(s.text)}</code></pre>`;
df3c0f2b3da4atagen2026-10-22 13:07 UTC77
      }
cf23dbf4e77catagen2026-10-23 06:36 UTC78
      return textToHtml(s.text, pattern, byText);
df3c0f2b3da4atagen2026-10-22 13:07 UTC79
    })
df3c0f2b3da4atagen2026-10-22 13:07 UTC80
    .join('');
df3c0f2b3da4atagen2026-10-22 13:07 UTC81
}