UNPKG

9.82 kBJavaScriptView Raw
1(function (Prism) {
2
3 var templateString = Prism.languages.javascript['template-string'];
4
5 // see the pattern in prism-javascript.js
6 var templateLiteralPattern = templateString.pattern.source;
7 var interpolationObject = templateString.inside['interpolation'];
8 var interpolationPunctuationObject = interpolationObject.inside['interpolation-punctuation'];
9 var interpolationPattern = interpolationObject.pattern.source;
10
11
12 /**
13 * Creates a new pattern to match a template string with a special tag.
14 *
15 * This will return `undefined` if there is no grammar with the given language id.
16 *
17 * @param {string} language The language id of the embedded language. E.g. `markdown`.
18 * @param {string} tag The regex pattern to match the tag.
19 * @returns {object | undefined}
20 * @example
21 * createTemplate('css', /\bcss/.source);
22 */
23 function createTemplate(language, tag) {
24 if (!Prism.languages[language]) {
25 return undefined;
26 }
27
28 return {
29 pattern: RegExp('((?:' + tag + ')\\s*)' + templateLiteralPattern),
30 lookbehind: true,
31 greedy: true,
32 inside: {
33 'template-punctuation': {
34 pattern: /^`|`$/,
35 alias: 'string'
36 },
37 'embedded-code': {
38 pattern: /[\s\S]+/,
39 alias: language
40 }
41 }
42 };
43 }
44
45
46 Prism.languages.javascript['template-string'] = [
47 // styled-jsx:
48 // css`a { color: #25F; }`
49 // styled-components:
50 // styled.h1`color: red;`
51 createTemplate('css', /\b(?:styled(?:\([^)]*\))?(?:\s*\.\s*\w+(?:\([^)]*\))*)*|css(?:\s*\.\s*(?:global|resolve))?|createGlobalStyle|keyframes)/.source),
52
53 // html`<p></p>`
54 // div.innerHTML = `<p></p>`
55 createTemplate('html', /\bhtml|\.\s*(?:inner|outer)HTML\s*\+?=/.source),
56
57 // svg`<path fill="#fff" d="M55.37 ..."/>`
58 createTemplate('svg', /\bsvg/.source),
59
60 // md`# h1`, markdown`## h2`
61 createTemplate('markdown', /\b(?:markdown|md)/.source),
62
63 // gql`...`, graphql`...`, graphql.experimental`...`
64 createTemplate('graphql', /\b(?:gql|graphql(?:\s*\.\s*experimental)?)/.source),
65
66 // sql`...`
67 createTemplate('sql', /\bsql/.source),
68
69 // vanilla template string
70 templateString
71 ].filter(Boolean);
72
73
74 /**
75 * Returns a specific placeholder literal for the given language.
76 *
77 * @param {number} counter
78 * @param {string} language
79 * @returns {string}
80 */
81 function getPlaceholder(counter, language) {
82 return '___' + language.toUpperCase() + '_' + counter + '___';
83 }
84
85 /**
86 * Returns the tokens of `Prism.tokenize` but also runs the `before-tokenize` and `after-tokenize` hooks.
87 *
88 * @param {string} code
89 * @param {any} grammar
90 * @param {string} language
91 * @returns {(string|Token)[]}
92 */
93 function tokenizeWithHooks(code, grammar, language) {
94 var env = {
95 code: code,
96 grammar: grammar,
97 language: language
98 };
99 Prism.hooks.run('before-tokenize', env);
100 env.tokens = Prism.tokenize(env.code, env.grammar);
101 Prism.hooks.run('after-tokenize', env);
102 return env.tokens;
103 }
104
105 /**
106 * Returns the token of the given JavaScript interpolation expression.
107 *
108 * @param {string} expression The code of the expression. E.g. `"${42}"`
109 * @returns {Token}
110 */
111 function tokenizeInterpolationExpression(expression) {
112 var tempGrammar = {};
113 tempGrammar['interpolation-punctuation'] = interpolationPunctuationObject;
114
115 /** @type {Array} */
116 var tokens = Prism.tokenize(expression, tempGrammar);
117 if (tokens.length === 3) {
118 /**
119 * The token array will look like this
120 * [
121 * ["interpolation-punctuation", "${"]
122 * "..." // JavaScript expression of the interpolation
123 * ["interpolation-punctuation", "}"]
124 * ]
125 */
126
127 var args = [1, 1];
128 args.push.apply(args, tokenizeWithHooks(tokens[1], Prism.languages.javascript, 'javascript'));
129
130 tokens.splice.apply(tokens, args);
131 }
132
133 return new Prism.Token('interpolation', tokens, interpolationObject.alias, expression);
134 }
135
136 /**
137 * Tokenizes the given code with support for JavaScript interpolation expressions mixed in.
138 *
139 * This function has 3 phases:
140 *
141 * 1. Replace all JavaScript interpolation expression with a placeholder.
142 * The placeholder will have the syntax of a identify of the target language.
143 * 2. Tokenize the code with placeholders.
144 * 3. Tokenize the interpolation expressions and re-insert them into the tokenize code.
145 * The insertion only works if a placeholder hasn't been "ripped apart" meaning that the placeholder has been
146 * tokenized as two tokens by the grammar of the embedded language.
147 *
148 * @param {string} code
149 * @param {object} grammar
150 * @param {string} language
151 * @returns {Token}
152 */
153 function tokenizeEmbedded(code, grammar, language) {
154 // 1. First filter out all interpolations
155
156 // because they might be escaped, we need a lookbehind, so we use Prism
157 /** @type {(Token|string)[]} */
158 var _tokens = Prism.tokenize(code, {
159 'interpolation': {
160 pattern: RegExp(interpolationPattern),
161 lookbehind: true
162 }
163 });
164
165 // replace all interpolations with a placeholder which is not in the code already
166 var placeholderCounter = 0;
167 /** @type {Object<string, string>} */
168 var placeholderMap = {};
169 var embeddedCode = _tokens.map(function (token) {
170 if (typeof token === 'string') {
171 return token;
172 } else {
173 var interpolationExpression = token.content;
174
175 var placeholder;
176 while (code.indexOf(placeholder = getPlaceholder(placeholderCounter++, language)) !== -1) { /* noop */ }
177 placeholderMap[placeholder] = interpolationExpression;
178 return placeholder;
179 }
180 }).join('');
181
182
183 // 2. Tokenize the embedded code
184
185 var embeddedTokens = tokenizeWithHooks(embeddedCode, grammar, language);
186
187
188 // 3. Re-insert the interpolation
189
190 var placeholders = Object.keys(placeholderMap);
191 placeholderCounter = 0;
192
193 /**
194 *
195 * @param {(Token|string)[]} tokens
196 * @returns {void}
197 */
198 function walkTokens(tokens) {
199 for (var i = 0; i < tokens.length; i++) {
200 if (placeholderCounter >= placeholders.length) {
201 return;
202 }
203
204 var token = tokens[i];
205
206 if (typeof token === 'string' || typeof token.content === 'string') {
207 var placeholder = placeholders[placeholderCounter];
208 var s = typeof token === 'string' ? token : /** @type {string} */ (token.content);
209
210 var index = s.indexOf(placeholder);
211 if (index !== -1) {
212 ++placeholderCounter;
213
214 var before = s.substring(0, index);
215 var middle = tokenizeInterpolationExpression(placeholderMap[placeholder]);
216 var after = s.substring(index + placeholder.length);
217
218 var replacement = [];
219 if (before) {
220 replacement.push(before);
221 }
222 replacement.push(middle);
223 if (after) {
224 var afterTokens = [after];
225 walkTokens(afterTokens);
226 replacement.push.apply(replacement, afterTokens);
227 }
228
229 if (typeof token === 'string') {
230 tokens.splice.apply(tokens, [i, 1].concat(replacement));
231 i += replacement.length - 1;
232 } else {
233 token.content = replacement;
234 }
235 }
236 } else {
237 var content = token.content;
238 if (Array.isArray(content)) {
239 walkTokens(content);
240 } else {
241 walkTokens([content]);
242 }
243 }
244 }
245 }
246 walkTokens(embeddedTokens);
247
248 return new Prism.Token(language, embeddedTokens, 'language-' + language, code);
249 }
250
251 /**
252 * The languages for which JS templating will handle tagged template literals.
253 *
254 * JS templating isn't active for only JavaScript but also related languages like TypeScript, JSX, and TSX.
255 */
256 var supportedLanguages = {
257 'javascript': true,
258 'js': true,
259 'typescript': true,
260 'ts': true,
261 'jsx': true,
262 'tsx': true,
263 };
264 Prism.hooks.add('after-tokenize', function (env) {
265 if (!(env.language in supportedLanguages)) {
266 return;
267 }
268
269 /**
270 * Finds and tokenizes all template strings with an embedded languages.
271 *
272 * @param {(Token | string)[]} tokens
273 * @returns {void}
274 */
275 function findTemplateStrings(tokens) {
276 for (var i = 0, l = tokens.length; i < l; i++) {
277 var token = tokens[i];
278
279 if (typeof token === 'string') {
280 continue;
281 }
282
283 var content = token.content;
284 if (!Array.isArray(content)) {
285 if (typeof content !== 'string') {
286 findTemplateStrings([content]);
287 }
288 continue;
289 }
290
291 if (token.type === 'template-string') {
292 /**
293 * A JavaScript template-string token will look like this:
294 *
295 * ["template-string", [
296 * ["template-punctuation", "`"],
297 * (
298 * An array of "string" and "interpolation" tokens. This is the simple string case.
299 * or
300 * ["embedded-code", "..."] This is the token containing the embedded code.
301 * It also has an alias which is the language of the embedded code.
302 * ),
303 * ["template-punctuation", "`"]
304 * ]]
305 */
306
307 var embedded = content[1];
308 if (content.length === 3 && typeof embedded !== 'string' && embedded.type === 'embedded-code') {
309 // get string content
310 var code = stringContent(embedded);
311
312 var alias = embedded.alias;
313 var language = Array.isArray(alias) ? alias[0] : alias;
314
315 var grammar = Prism.languages[language];
316 if (!grammar) {
317 // the embedded language isn't registered.
318 continue;
319 }
320
321 content[1] = tokenizeEmbedded(code, grammar, language);
322 }
323 } else {
324 findTemplateStrings(content);
325 }
326 }
327 }
328
329 findTemplateStrings(env.tokens);
330 });
331
332
333 /**
334 * Returns the string content of a token or token stream.
335 *
336 * @param {string | Token | (string | Token)[]} value
337 * @returns {string}
338 */
339 function stringContent(value) {
340 if (typeof value === 'string') {
341 return value;
342 } else if (Array.isArray(value)) {
343 return value.map(stringContent).join('');
344 } else {
345 return stringContent(value.content);
346 }
347 }
348
349}(Prism));