'use strict'
module.exports = jsTemplates
jsTemplates.displayName = 'jsTemplates'
jsTemplates.aliases = []
function jsTemplates(Prism) {
;(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)
}