UNPKG

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