File size: 1,933 Bytes
cfb0fa4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// mention-extension.ts
type MentionOptions = {
	triggerChar?: string; // default "@"
	className?: string; // default "mention"
	extraAttrs?: Record<string, string>; // additional HTML attrs
};

function escapeHtml(s: string) {
	return s.replace(
		/[&<>"']/g,
		(c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c]!
	);
}

function mentionStart(src: string) {
	// Find the first "<" followed by trigger char
	// We'll refine inside tokenizer
	return src.indexOf('<');
}

function mentionTokenizer(this: any, src: string, options: MentionOptions = {}) {
	const trigger = options.triggerChar ?? '@';
	// Build dynamic regex for `<@id>`, `<@id|label>`, `<@id|>`
	// Added forward slash (/) to the character class for IDs
	const re = new RegExp(`^<\\${trigger}([\\w.\\-:/]+)(?:\\|([^>]*))?>`);
	const m = re.exec(src);
	if (!m) return;

	const [, id, label] = m;
	return {
		type: 'mention',
		raw: m[0],
		triggerChar: trigger,
		id,
		label: label && label.length > 0 ? label : id
	};
}

function mentionRenderer(token: any, options: MentionOptions = {}) {
	const trigger = options.triggerChar ?? '@';
	const cls = options.className ?? 'mention';
	const extra = options.extraAttrs ?? {};

	const attrs = Object.entries({
		class: cls,
		'data-type': 'mention',
		'data-id': token.id,
		'data-mention-suggestion-char': trigger,
		...extra
	})
		.map(([k, v]) => `${k}="${escapeHtml(String(v))}"`)
		.join(' ');

	return `<span ${attrs}>${escapeHtml(trigger + token.label)}</span>`;
}

export function mentionExtension(opts: MentionOptions = {}) {
	return {
		name: 'mention',
		level: 'inline' as const,
		start: mentionStart,
		tokenizer(src: string) {
			return mentionTokenizer.call(this, src, opts);
		},
		renderer(token: any) {
			return mentionRenderer(token, opts);
		}
	};
}

// Usage:
// import { marked } from 'marked';
// marked.use({ extensions: [mentionExtension({ triggerChar: '@' })] });