atagen

malice

public
Star0
Watch0
df3c0f2b3da49cedd2da20209bd76e39695179bc
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git
web/src/lib/code.tsTypescript58 lines1.9 KB
1
export type CodeSegment =
2
  | { kind: 'text'; text: string }
3
  | { kind: 'inline'; text: string }
4
  | { kind: 'block'; text: string; lang: string | null };
5
6
const FENCE = /```([^\s`]*)[^\S\n]*\n?([\s\S]*?)\n?```/g;
7
const INLINE = /`([^`\n]+)`/g;
8
9
function splitInline(text: string, out: CodeSegment[]): void {
10
  let last = 0;
11
  for (const m of text.matchAll(INLINE)) {
12
    if (m.index > last) out.push({ kind: 'text', text: text.slice(last, m.index) });
13
    out.push({ kind: 'inline', text: m[1] });
14
    last = m.index + m[0].length;
15
  }
16
  if (last < text.length) out.push({ kind: 'text', text: text.slice(last) });
17
}
18
19
export function parseCode(text: string): CodeSegment[] {
20
  const out: CodeSegment[] = [];
21
  if (!text.includes('`')) {
22
    if (text) out.push({ kind: 'text', text });
23
    return out;
24
  }
25
  let last = 0;
26
  for (const m of text.matchAll(FENCE)) {
27
    splitInline(text.slice(last, m.index), out);
28
    out.push({ kind: 'block', text: m[2], lang: m[1] || null });
29
    last = m.index + m[0].length;
30
    if (text[last] === '\n') last += 1;
31
  }
32
  splitInline(text.slice(last), out);
33
  return out;
34
}
35
36
export function hasCode(segments: CodeSegment[]): boolean {
37
  return segments.some((s) => s.kind !== 'text');
38
}
39
40
function escapeHtml(text: string): string {
41
  return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
42
}
43
44
/** Matrix `formatted_body` for a plain body, or null when it has no code to format. */
45
export function codeToHtml(body: string): string | null {
46
  const segments = parseCode(body);
47
  if (!hasCode(segments)) return null;
48
  return segments
49
    .map((s) => {
50
      if (s.kind === 'inline') return `<code>${escapeHtml(s.text)}</code>`;
51
      if (s.kind === 'block') {
52
        const cls = s.lang ? ` class="language-${escapeHtml(s.lang)}"` : '';
53
        return `<pre><code${cls}>${escapeHtml(s.text)}</code></pre>`;
54
      }
55
      return escapeHtml(s.text).replace(/\n/g, '<br>');
56
    })
57
    .join('');
58
}