350 lines
9.6 KiB
JavaScript
350 lines
9.6 KiB
JavaScript
|
(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`<p></p>`
|
||
|
// div.innerHTML = `<p></p>`
|
||
|
createTemplate('html', /\bhtml|\.\s*(?:inner|outer)HTML\s*\+?=/.source),
|
||
|
|
||
|
// svg`<path fill="#fff" d="M55.37 ..."/>`
|
||
|
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<string, string>} */
|
||
|
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));
|