atagen

malice

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

Blame: web/src/lib/markdown.ts

6d6a4c34bdc1atagen2026-10-24 06:51 UTC1
import { Marked, type Token, type Tokens } from 'marked';
6d6a4c34bdc1atagen2026-10-24 06:51 UTC2
import type { Mention } from './mentions';
6d6a4c34bdc1atagen2026-10-24 06:51 UTC3
6d6a4c34bdc1atagen2026-10-24 06:51 UTC4
function escapeHtml(text: string): string {
6d6a4c34bdc1atagen2026-10-24 06:51 UTC5
  return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
6d6a4c34bdc1atagen2026-10-24 06:51 UTC6
}
6d6a4c34bdc1atagen2026-10-24 06:51 UTC7
6d6a4c34bdc1atagen2026-10-24 06:51 UTC8
function escapeRegExp(text: string): string {
6d6a4c34bdc1atagen2026-10-24 06:51 UTC9
  return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
6d6a4c34bdc1atagen2026-10-24 06:51 UTC10
}
6d6a4c34bdc1atagen2026-10-24 06:51 UTC11
6d6a4c34bdc1atagen2026-10-24 06:51 UTC12
function mentionLinker(mentions: Mention[]): (escaped: string) => string {
6d6a4c34bdc1atagen2026-10-24 06:51 UTC13
  const byText = new Map(mentions.filter((m) => m.text).map((m) => [escapeHtml(m.text), m.user_id]));
6d6a4c34bdc1atagen2026-10-24 06:51 UTC14
  if (byText.size === 0) return (escaped) => escaped;
6d6a4c34bdc1atagen2026-10-24 06:51 UTC15
  const names = [...byText.keys()].sort((a, b) => b.length - a.length);
6d6a4c34bdc1atagen2026-10-24 06:51 UTC16
  const pattern = new RegExp(names.map(escapeRegExp).join('|'), 'g');
6d6a4c34bdc1atagen2026-10-24 06:51 UTC17
  return (escaped) =>
6d6a4c34bdc1atagen2026-10-24 06:51 UTC18
    escaped.replace(pattern, (name) => `<a href="https://matrix.to/#/${escapeHtml(byText.get(name)!)}">${name}</a>`);
6d6a4c34bdc1atagen2026-10-24 06:51 UTC19
}
6d6a4c34bdc1atagen2026-10-24 06:51 UTC20
6d6a4c34bdc1atagen2026-10-24 06:51 UTC21
function textToHtml(text: string, link: (escaped: string) => string): string {
6d6a4c34bdc1atagen2026-10-24 06:51 UTC22
  return link(escapeHtml(text)).replace(/\n/g, '<br>');
6d6a4c34bdc1atagen2026-10-24 06:51 UTC23
}
6d6a4c34bdc1atagen2026-10-24 06:51 UTC24
6d6a4c34bdc1atagen2026-10-24 06:51 UTC25
const PLAIN_INLINE = new Set(['text', 'escape', 'br']);
6d6a4c34bdc1atagen2026-10-24 06:51 UTC26
6d6a4c34bdc1atagen2026-10-24 06:51 UTC27
function isPlain(tokens: Token[]): boolean {
6d6a4c34bdc1atagen2026-10-24 06:51 UTC28
  const blocks = tokens.filter((t) => t.type !== 'space');
6d6a4c34bdc1atagen2026-10-24 06:51 UTC29
  if (blocks.length !== 1 || blocks[0].type !== 'paragraph') return blocks.length === 0;
6d6a4c34bdc1atagen2026-10-24 06:51 UTC30
  return (blocks[0] as Tokens.Paragraph).tokens.every((t) => PLAIN_INLINE.has(t.type) && !('tokens' in t && t.tokens));
6d6a4c34bdc1atagen2026-10-24 06:51 UTC31
}
6d6a4c34bdc1atagen2026-10-24 06:51 UTC32
6d6a4c34bdc1atagen2026-10-24 06:51 UTC33
function markdownToHtml(body: string, link: (escaped: string) => string): string | null {
6d6a4c34bdc1atagen2026-10-24 06:51 UTC34
  const marked = new Marked({ gfm: true, breaks: true });
6d6a4c34bdc1atagen2026-10-24 06:51 UTC35
  marked.use({
6d6a4c34bdc1atagen2026-10-24 06:51 UTC36
    renderer: {
6d6a4c34bdc1atagen2026-10-24 06:51 UTC37
      text(token) {
6d6a4c34bdc1atagen2026-10-24 06:51 UTC38
        if ('tokens' in token && token.tokens) return false;
6d6a4c34bdc1atagen2026-10-24 06:51 UTC39
        return link('escaped' in token && token.escaped ? token.text : escapeHtml(token.text));
6d6a4c34bdc1atagen2026-10-24 06:51 UTC40
      },
6d6a4c34bdc1atagen2026-10-24 06:51 UTC41
      html(token) {
6d6a4c34bdc1atagen2026-10-24 06:51 UTC42
        return escapeHtml(token.text);
6d6a4c34bdc1atagen2026-10-24 06:51 UTC43
      },
6d6a4c34bdc1atagen2026-10-24 06:51 UTC44
      image(token) {
6d6a4c34bdc1atagen2026-10-24 06:51 UTC45
        return `<a href="${escapeHtml(token.href)}">${escapeHtml(token.text || token.href)}</a>`;
6d6a4c34bdc1atagen2026-10-24 06:51 UTC46
      },
6d6a4c34bdc1atagen2026-10-24 06:51 UTC47
      checkbox(token) {
6d6a4c34bdc1atagen2026-10-24 06:51 UTC48
        return token.checked ? '[x] ' : '[ ] ';
6d6a4c34bdc1atagen2026-10-24 06:51 UTC49
      }
6d6a4c34bdc1atagen2026-10-24 06:51 UTC50
    }
6d6a4c34bdc1atagen2026-10-24 06:51 UTC51
  });
6d6a4c34bdc1atagen2026-10-24 06:51 UTC52
  const tokens = marked.lexer(body);
6d6a4c34bdc1atagen2026-10-24 06:51 UTC53
  if (isPlain(tokens)) return null;
6d6a4c34bdc1atagen2026-10-24 06:51 UTC54
  return marked.parser(tokens).trim();
6d6a4c34bdc1atagen2026-10-24 06:51 UTC55
}
6d6a4c34bdc1atagen2026-10-24 06:51 UTC56
6d6a4c34bdc1atagen2026-10-24 06:51 UTC57
/** Matrix `formatted_body` for a composed message, or null when plain text says it all. */
6d6a4c34bdc1atagen2026-10-24 06:51 UTC58
export function formatBody(body: string, mentions: Mention[], markdown: boolean): string | null {
6d6a4c34bdc1atagen2026-10-24 06:51 UTC59
  const link = mentionLinker(mentions);
6d6a4c34bdc1atagen2026-10-24 06:51 UTC60
  const html = markdown ? markdownToHtml(body, link) : null;
6d6a4c34bdc1atagen2026-10-24 06:51 UTC61
  if (html !== null) return html;
6d6a4c34bdc1atagen2026-10-24 06:51 UTC62
  const plain = textToHtml(body, link);
6d6a4c34bdc1atagen2026-10-24 06:51 UTC63
  return plain === escapeHtml(body).replace(/\n/g, '<br>') ? null : plain;
6d6a4c34bdc1atagen2026-10-24 06:51 UTC64
}
6d6a4c34bdc1atagen2026-10-24 06:51 UTC65
6d6a4c34bdc1atagen2026-10-24 06:51 UTC66
export function insideOpenFence(text: string, caret: number): boolean {
6d6a4c34bdc1atagen2026-10-24 06:51 UTC67
  return (text.slice(0, caret).match(/^\s*```/gm)?.length ?? 0) % 2 === 1;
6d6a4c34bdc1atagen2026-10-24 06:51 UTC68
}