UNPKG

5.2 kBPlain TextView Raw
1import { readFile } from 'fs-extra';
2
3import { tableFile, aliasesFile, asciiFile, charactersFile } from './build';
4
5const buster = require.main.require('./src/meta').config['cache-buster'];
6const nconf = require.main.require('nconf');
7const winston = require.main.require('winston');
8const url = nconf.get('url');
9
10let metaCache: {
11 table: MetaData.Table;
12 aliases: MetaData.Aliases;
13 ascii: MetaData.Ascii;
14 asciiPattern: RegExp;
15 characters: MetaData.Characters;
16 charPattern: RegExp;
17} = null;
18export function clearCache() {
19 metaCache = null;
20}
21
22const escapeRegExpChars = (text: string) => text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
23
24const getTable = async (): Promise<typeof metaCache> => {
25 if (metaCache) {
26 return metaCache;
27 }
28
29 const [
30 tableText,
31 aliasesText,
32 asciiText,
33 charactersText,
34 ]: [string, string, string, string] = await Promise.all([
35 readFile(tableFile, 'utf8'),
36 readFile(aliasesFile, 'utf8'),
37 readFile(asciiFile, 'utf8'),
38 readFile(charactersFile, 'utf8'),
39 ]);
40
41 const table = JSON.parse(tableText);
42 const aliases = JSON.parse(aliasesText);
43 const ascii = JSON.parse(asciiText);
44 const characters = JSON.parse(charactersText);
45
46 const asciiPattern = Object.keys(ascii)
47 .sort((a, b) => b.length - a.length)
48 .map(escapeRegExpChars)
49 .join('|');
50 const charPattern = Object.keys(characters)
51 .sort((a, b) => b.length - a.length)
52 .map(escapeRegExpChars)
53 .join('|');
54
55 metaCache = {
56 table,
57 aliases,
58 ascii,
59 characters,
60 asciiPattern: asciiPattern ?
61 new RegExp(`(^|\\s|\\n)(${asciiPattern})(?=\\n|\\s|$)`, 'g') :
62 /(?!)/,
63 charPattern: charPattern ?
64 new RegExp(charPattern, 'g') :
65 /(?!)/,
66 };
67
68 return metaCache;
69};
70
71const outsideCode = /(^|<\/code>)([^<]*|<(?!code[^>]*>))*(<code[^>]*>|$)/g;
72const outsideElements = /(<[^>]*>)?([^<>]*)/g;
73const emojiPattern = /:([a-z\-.+0-9_]+):/g;
74
75export const buildEmoji = (emoji: StoredEmoji, whole: string) => {
76 if (emoji.image) {
77 const route = `${url}/plugins/nodebb-plugin-emoji/emoji/${emoji.pack}`;
78 return `<img
79 src="${route}/${emoji.image}?${buster}"
80 class="not-responsive emoji emoji-${emoji.pack} emoji--${emoji.name}"
81 title="${whole}"
82 alt="${emoji.character}"
83 />`;
84 }
85
86 return `<span
87 class="emoji emoji-${emoji.pack} emoji--${emoji.name}"
88 title="${whole}"
89 ><span>${emoji.character}</span></span>`;
90};
91
92const replaceAscii = (
93 str: string,
94 { ascii, asciiPattern, table }: (typeof metaCache)
95) => str.replace(asciiPattern, (full: string, before: string, text: string) => {
96 const emoji = ascii[text] && table[ascii[text]];
97 if (emoji) {
98 return `${before}${buildEmoji(emoji, text)}`;
99 }
100
101 return full;
102});
103
104const replaceNative = (
105 str: string,
106 { characters, charPattern, table }: (typeof metaCache)
107) => str.replace(charPattern, (char: string) => {
108 const name = characters[char];
109 if (table[name]) {
110 return `:${name}:`;
111 }
112
113 return char;
114});
115
116interface ParseOptions {
117 /** whether to parse ascii emoji representations into emoji */
118 ascii?: boolean;
119 native?: boolean;
120}
121
122const options: ParseOptions = {
123 ascii: false,
124 native: false,
125};
126
127export function setOptions(newOptions: ParseOptions) {
128 Object.assign(options, newOptions);
129}
130
131const parse = async (content: string): Promise<string> => {
132 let store: typeof metaCache;
133 try {
134 store = await getTable();
135 } catch (err) {
136 winston.error('[emoji] Failed to retrieve data for parse', err);
137 return content;
138 }
139 const { table, aliases } = store;
140
141 const parsed = content.replace(
142 outsideCode,
143 (outsideCodeStr) => outsideCodeStr.replace(outsideElements, (_, inside, outside) => {
144 let output = outside;
145
146 if (options.native) {
147 // avoid parsing native inside HTML tags
148 // also avoid converting ascii characters
149 output = output.replace(
150 /(<[^>]+>)|([^0-9a-zA-Z`~!@#$%^&*()\-=_+{}|[\]\\:";'<>?,./\s\n]+)/g,
151 (full: string, tag: string, text: string) => {
152 if (text) {
153 return replaceNative(text, store);
154 }
155
156 return full;
157 }
158 );
159 }
160
161 output = output.replace(emojiPattern, (whole: string, text: string) => {
162 const name = text.toLowerCase();
163 const emoji = table[name] || table[aliases[name]];
164
165 if (emoji) {
166 return buildEmoji(emoji, whole);
167 }
168
169 return whole;
170 });
171
172 if (options.ascii) {
173 // avoid parsing native inside HTML tags
174 output = output.replace(
175 /(<[^>]+>)|([^<]+)/g,
176 (full: string, tag: string, text: string) => {
177 if (text) {
178 return replaceAscii(text, store);
179 }
180
181 return full;
182 }
183 );
184 }
185
186 return (inside || '') + (output || '');
187 })
188 );
189
190 return parsed;
191};
192
193export function raw(content: string): Promise<string> {
194 return parse(content);
195}
196
197export async function post(data: { postData: { content: string } }): Promise<any> {
198 // eslint-disable-next-line no-param-reassign
199 data.postData.content = await parse(data.postData.content);
200 return data;
201}
202
\No newline at end of file