| | |
| | type MentionOptions = { |
| | triggerChar?: string; |
| | className?: string; |
| | extraAttrs?: Record<string, string>; |
| | }; |
| |
|
| | function escapeHtml(s: string) { |
| | return s.replace( |
| | /[&<>"']/g, |
| | (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]! |
| | ); |
| | } |
| |
|
| | function mentionStart(src: string) { |
| | |
| | |
| | return src.indexOf('<'); |
| | } |
| |
|
| | function mentionTokenizer(this: any, src: string, options: MentionOptions = {}) { |
| | const trigger = options.triggerChar ?? '@'; |
| | |
| | |
| | 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); |
| | } |
| | }; |
| | } |
| |
|
| | |
| | |
| | |
| |
|