'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) }