(function (Prism) {
var templateString = Prism.languages.javascript['template-string'];
// see the pattern in prism-javascript.js
var templateLiteralPattern = templateString.pattern.source;
var interpolationObject = templateString.inside['interpolation'];
var interpolationPunctuationObject = interpolationObject.inside['interpolation-punctuation'];
var interpolationPattern = interpolationObject.pattern.source;
/**
* Creates a new pattern to match a template string with a special tag.
*
* This will return `undefined` if there is no grammar with the given language id.
*
* @param {string} language The language id of the embedded language. E.g. `markdown`.
* @param {string} tag The regex pattern to match the tag.
* @returns {object | undefined}
* @example
* createTemplate('css', /\bcss/.source);
*/
function createTemplate(language, tag) {
if (!Prism.languages[language]) {
return undefined;
}
return {
pattern: RegExp('((?:' + tag + ')\\s*)' + templateLiteralPattern),
lookbehind: true,
greedy: true,
inside: {
'template-punctuation': {
pattern: /^`|`$/,
alias: 'string'
},
'embedded-code': {
pattern: /[\s\S]+/,
alias: language
}
}
};
}
Prism.languages.javascript['template-string'] = [
// styled-jsx:
// css`a { color: #25F; }`
// styled-components:
// styled.h1`color: red;`
createTemplate('css', /\b(?:styled(?:\([^)]*\))?(?:\s*\.\s*\w+(?:\([^)]*\))*)*|css(?:\s*\.\s*(?:global|resolve))?|createGlobalStyle|keyframes)/.source),
// html`
`
// div.innerHTML = ``
createTemplate('html', /\bhtml|\.\s*(?:inner|outer)HTML\s*\+?=/.source),
// svg``
createTemplate('svg', /\bsvg/.source),
// md`# h1`, markdown`## h2`
createTemplate('markdown', /\b(?:markdown|md)/.source),
// gql`...`, graphql`...`, graphql.experimental`...`
createTemplate('graphql', /\b(?:gql|graphql(?:\s*\.\s*experimental)?)/.source),
// sql`...`
createTemplate('sql', /\bsql/.source),
// vanilla template string
templateString
].filter(Boolean);
/**
* Returns a specific placeholder literal for the given language.
*
* @param {number} counter
* @param {string} language
* @returns {string}
*/
function getPlaceholder(counter, language) {
return '___' + language.toUpperCase() + '_' + counter + '___';
}
/**
* Returns the tokens of `Prism.tokenize` but also runs the `before-tokenize` and `after-tokenize` hooks.
*
* @param {string} code
* @param {any} grammar
* @param {string} language
* @returns {(string|Token)[]}
*/
function tokenizeWithHooks(code, grammar, language) {
var env = {
code: code,
grammar: grammar,
language: language
};
Prism.hooks.run('before-tokenize', env);
env.tokens = Prism.tokenize(env.code, env.grammar);
Prism.hooks.run('after-tokenize', env);
return env.tokens;
}
/**
* Returns the token of the given JavaScript interpolation expression.
*
* @param {string} expression The code of the expression. E.g. `"${42}"`
* @returns {Token}
*/
function tokenizeInterpolationExpression(expression) {
var tempGrammar = {};
tempGrammar['interpolation-punctuation'] = interpolationPunctuationObject;
/** @type {Array} */
var tokens = Prism.tokenize(expression, tempGrammar);
if (tokens.length === 3) {
/**
* The token array will look like this
* [
* ["interpolation-punctuation", "${"]
* "..." // JavaScript expression of the interpolation
* ["interpolation-punctuation", "}"]
* ]
*/
var args = [1, 1];
args.push.apply(args, tokenizeWithHooks(tokens[1], Prism.languages.javascript, 'javascript'));
tokens.splice.apply(tokens, args);
}
return new Prism.Token('interpolation', tokens, interpolationObject.alias, expression);
}
/**
* Tokenizes the given code with support for JavaScript interpolation expressions mixed in.
*
* This function has 3 phases:
*
* 1. Replace all JavaScript interpolation expression with a placeholder.
* The placeholder will have the syntax of a identify of the target language.
* 2. Tokenize the code with placeholders.
* 3. Tokenize the interpolation expressions and re-insert them into the tokenize code.
* The insertion only works if a placeholder hasn't been "ripped apart" meaning that the placeholder has been
* tokenized as two tokens by the grammar of the embedded language.
*
* @param {string} code
* @param {object} grammar
* @param {string} language
* @returns {Token}
*/
function tokenizeEmbedded(code, grammar, language) {
// 1. First filter out all interpolations
// because they might be escaped, we need a lookbehind, so we use Prism
/** @type {(Token|string)[]} */
var _tokens = Prism.tokenize(code, {
'interpolation': {
pattern: RegExp(interpolationPattern),
lookbehind: true
}
});
// replace all interpolations with a placeholder which is not in the code already
var placeholderCounter = 0;
/** @type {Object} */
var placeholderMap = {};
var embeddedCode = _tokens.map(function (token) {
if (typeof token === 'string') {
return token;
} else {
var interpolationExpression = token.content;
var placeholder;
while (code.indexOf(placeholder = getPlaceholder(placeholderCounter++, language)) !== -1) { /* noop */ }
placeholderMap[placeholder] = interpolationExpression;
return placeholder;
}
}).join('');
// 2. Tokenize the embedded code
var embeddedTokens = tokenizeWithHooks(embeddedCode, grammar, language);
// 3. Re-insert the interpolation
var placeholders = Object.keys(placeholderMap);
placeholderCounter = 0;
/**
*
* @param {(Token|string)[]} tokens
* @returns {void}
*/
function walkTokens(tokens) {
for (var i = 0; i < tokens.length; i++) {
if (placeholderCounter >= placeholders.length) {
return;
}
var token = tokens[i];
if (typeof token === 'string' || typeof token.content === 'string') {
var placeholder = placeholders[placeholderCounter];
var s = typeof token === 'string' ? token : /** @type {string} */ (token.content);
var index = s.indexOf(placeholder);
if (index !== -1) {
++placeholderCounter;
var before = s.substring(0, index);
var middle = tokenizeInterpolationExpression(placeholderMap[placeholder]);
var after = s.substring(index + placeholder.length);
var replacement = [];
if (before) {
replacement.push(before);
}
replacement.push(middle);
if (after) {
var afterTokens = [after];
walkTokens(afterTokens);
replacement.push.apply(replacement, afterTokens);
}
if (typeof token === 'string') {
tokens.splice.apply(tokens, [i, 1].concat(replacement));
i += replacement.length - 1;
} else {
token.content = replacement;
}
}
} else {
var content = token.content;
if (Array.isArray(content)) {
walkTokens(content);
} else {
walkTokens([content]);
}
}
}
}
walkTokens(embeddedTokens);
return new Prism.Token(language, embeddedTokens, 'language-' + language, code);
}
/**
* The languages for which JS templating will handle tagged template literals.
*
* JS templating isn't active for only JavaScript but also related languages like TypeScript, JSX, and TSX.
*/
var supportedLanguages = {
'javascript': true,
'js': true,
'typescript': true,
'ts': true,
'jsx': true,
'tsx': true,
};
Prism.hooks.add('after-tokenize', function (env) {
if (!(env.language in supportedLanguages)) {
return;
}
/**
* Finds and tokenizes all template strings with an embedded languages.
*
* @param {(Token | string)[]} tokens
* @returns {void}
*/
function findTemplateStrings(tokens) {
for (var i = 0, l = tokens.length; i < l; i++) {
var token = tokens[i];
if (typeof token === 'string') {
continue;
}
var content = token.content;
if (!Array.isArray(content)) {
if (typeof content !== 'string') {
findTemplateStrings([content]);
}
continue;
}
if (token.type === 'template-string') {
/**
* A JavaScript template-string token will look like this:
*
* ["template-string", [
* ["template-punctuation", "`"],
* (
* An array of "string" and "interpolation" tokens. This is the simple string case.
* or
* ["embedded-code", "..."] This is the token containing the embedded code.
* It also has an alias which is the language of the embedded code.
* ),
* ["template-punctuation", "`"]
* ]]
*/
var embedded = content[1];
if (content.length === 3 && typeof embedded !== 'string' && embedded.type === 'embedded-code') {
// get string content
var code = stringContent(embedded);
var alias = embedded.alias;
var language = Array.isArray(alias) ? alias[0] : alias;
var grammar = Prism.languages[language];
if (!grammar) {
// the embedded language isn't registered.
continue;
}
content[1] = tokenizeEmbedded(code, grammar, language);
}
} else {
findTemplateStrings(content);
}
}
}
findTemplateStrings(env.tokens);
});
/**
* Returns the string content of a token or token stream.
*
* @param {string | Token | (string | Token)[]} value
* @returns {string}
*/
function stringContent(value) {
if (typeof value === 'string') {
return value;
} else if (Array.isArray(value)) {
return value.map(stringContent).join('');
} else {
return stringContent(value.content);
}
}
}(Prism));