-
Notifications
You must be signed in to change notification settings - Fork 20
/
Copy pathindex.ts
115 lines (100 loc) · 3.5 KB
/
index.ts
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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
import { get as getEmoji } from 'node-emoji';
import { emoticon } from 'emoticon';
import { findAndReplace, type Find, type Replace } from 'mdast-util-find-and-replace';
import type { Plugin } from 'unified';
import type { Root, Nodes, Text } from 'mdast';
const RE_EMOJI = /:\+1:|:-1:|:[\w-]+:/g;
const RE_SHORT = /[$@|*'",;.=:\-)([\]\\/<>038BOopPsSdDxXzZ]{2,5}/g;
const RE_PUNCT = /(?:_|-(?!1))/g;
/**
* Configuration of remark-emoji plugin.
*/
export interface RemarkEmojiOptions {
/**
* Makes converted emoji and emoticon texts accessible by wrapping them with
* `span` element setting `role` and `aria-label` attributes.
*
* @defaultValue false
*/
accessible?: boolean;
/**
* Adds an extra whitespace after emoji.
* Useful when browser handle emojis with half character length and
* the following character is hidden.
*
* @defaultValue false
*/
padSpaceAfter?: boolean;
/**
* Whether to support emoticon shortcodes (e.g. :-) will be replaced by 😃)
*
* @defaultValue false
*/
emoticon?: boolean;
}
const DEFAULT_SETTINGS: RemarkEmojiOptions = {
padSpaceAfter: false,
emoticon: false,
accessible: false,
};
const plugin: Plugin<[(RemarkEmojiOptions | null | undefined)?], Root> = options => {
const settings = Object.assign({}, DEFAULT_SETTINGS, options);
const pad = !!settings.padSpaceAfter;
const emoticonEnable = !!settings.emoticon;
const accessible = !!settings.accessible;
function aria(text: string, label: string): Text {
// Creating HTML node in Markdown node is undocumented.
// https://github.com/syntax-tree/mdast-util-math/blob/e70bb824dc70f5423324b31b0b68581cf6698fe8/index.js#L44-L55
return {
type: 'text',
value: text,
data: {
hName: 'span',
hProperties: {
role: 'img',
ariaLabel: label,
},
hChildren: [{ type: 'text', value: text }],
},
};
}
function replaceEmoticon(match: string): string | false | Text {
// find emoji by shortcode - full match or with-out last char as it could be from text e.g. :-),
const iconFull = emoticon.find(e => e.emoticons.includes(match)); // full match
const iconPart = emoticon.find(e => e.emoticons.includes(match.slice(0, -1))); // second search pattern
const icon = iconFull || iconPart;
if (!icon) {
return false;
}
const trimmedChar = !iconFull && iconPart ? match.slice(-1) : '';
const addPad = pad ? ' ' : '';
const replaced = icon.emoji + addPad + trimmedChar;
if (accessible) {
return aria(replaced, icon.name + ' emoticon');
}
return replaced;
}
function replaceEmoji(match: string): string | false | Text {
let got = getEmoji(match);
if (typeof got === 'undefined') {
return false;
}
if (pad) {
got = got + ' ';
}
if (accessible) {
const label = match.slice(1, -1).replace(RE_PUNCT, ' ') + ' emoji';
return aria(got, label);
}
return got;
}
const replacers: [Find, Replace][] = [[RE_EMOJI, replaceEmoji]];
if (emoticonEnable) {
replacers.push([RE_SHORT, replaceEmoticon]);
}
function transformer(tree: Nodes): void {
findAndReplace(tree, replacers);
}
return transformer;
};
export default plugin;