1 | import { readFile } from 'fs-extra';
|
2 |
|
3 | import { tableFile, aliasesFile, asciiFile, charactersFile } from './build';
|
4 |
|
5 | const buster = require.main.require('./src/meta').config['cache-buster'];
|
6 | const nconf = require.main.require('nconf');
|
7 | const winston = require.main.require('winston');
|
8 | const url = nconf.get('url');
|
9 |
|
10 | let 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;
|
18 | export function clearCache() {
|
19 | metaCache = null;
|
20 | }
|
21 |
|
22 | const escapeRegExpChars = (text: string) => text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
|
23 |
|
24 | const 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 |
|
71 | const outsideCode = /(^|<\/code>)([^<]*|<(?!code[^>]*>))*(<code[^>]*>|$)/g;
|
72 | const outsideElements = /(<[^>]*>)?([^<>]*)/g;
|
73 | const emojiPattern = /:([a-z\-.+0-9_]+):/g;
|
74 |
|
75 | export 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 |
|
92 | const 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 |
|
104 | const 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 |
|
116 | interface ParseOptions {
|
117 |
|
118 | ascii?: boolean;
|
119 | native?: boolean;
|
120 | }
|
121 |
|
122 | const options: ParseOptions = {
|
123 | ascii: false,
|
124 | native: false,
|
125 | };
|
126 |
|
127 | export function setOptions(newOptions: ParseOptions) {
|
128 | Object.assign(options, newOptions);
|
129 | }
|
130 |
|
131 | const 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 |
|
148 |
|
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 |
|
193 | export function raw(content: string): Promise<string> {
|
194 | return parse(content);
|
195 | }
|
196 |
|
197 | export 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 |