/* Language: JavaScript Description: JavaScript (JS) is a lightweight, interpreted, or just-in-time compiled programming language with first-class functions. Category: common, scripting, web Website: https://developer.mozilla.org/en-US/docs/Web/JavaScript */ import * as ECMAScript from './lib/ecmascript.js'; /** @type LanguageFn */ export default function(hljs) { const regex = hljs.regex; /** * Takes a string like " { const tag = "', end: '' }; // to avoid some special cases inside isTrulyOpeningTag const XML_SELF_CLOSING = /<[A-Za-z0-9\\._:-]+\s*\/>/; const XML_TAG = { begin: /<[A-Za-z0-9\\._:-]+/, end: /\/[A-Za-z0-9\\._:-]+>|\/>/, /** * @param {RegExpMatchArray} match * @param {CallbackResponse} response */ isTrulyOpeningTag: (match, response) => { const afterMatchIndex = match[0].length + match.index; const nextChar = match.input[afterMatchIndex]; if ( // HTML should not include another raw `<` inside a tag // nested type? // `>`, etc. nextChar === "<" || // the , gives away that this is not HTML // `` nextChar === "," ) { response.ignoreMatch(); return; } // `` // Quite possibly a tag, lets look for a matching closing tag... if (nextChar === ">") { // if we cannot find a matching closing tag, then we // will ignore it if (!hasClosingTag(match, { after: afterMatchIndex })) { response.ignoreMatch(); } } // `` (self-closing) // handled by simpleSelfClosing rule let m; const afterMatch = match.input.substring(afterMatchIndex); // some more template typing stuff // (key?: string) => Modify< if ((m = afterMatch.match(/^\s*=/))) { response.ignoreMatch(); return; } // `` // technically this could be HTML, but it smells like a type // NOTE: This is ugh, but added specifically for https://github.com/highlightjs/highlight.js/issues/3276 if ((m = afterMatch.match(/^\s+extends\s+/))) { if (m.index === 0) { response.ignoreMatch(); // eslint-disable-next-line no-useless-return return; } } } }; const KEYWORDS = { $pattern: ECMAScript.IDENT_RE, keyword: ECMAScript.KEYWORDS, literal: ECMAScript.LITERALS, built_in: ECMAScript.BUILT_INS, "variable.language": ECMAScript.BUILT_IN_VARIABLES }; // https://tc39.es/ecma262/#sec-literals-numeric-literals const decimalDigits = '[0-9](_?[0-9])*'; const frac = `\\.(${decimalDigits})`; // DecimalIntegerLiteral, including Annex B NonOctalDecimalIntegerLiteral // https://tc39.es/ecma262/#sec-additional-syntax-numeric-literals const decimalInteger = `0|[1-9](_?[0-9])*|0[0-7]*[89][0-9]*`; const NUMBER = { className: 'number', variants: [ // DecimalLiteral { begin: `(\\b(${decimalInteger})((${frac})|\\.)?|(${frac}))` + `[eE][+-]?(${decimalDigits})\\b` }, { begin: `\\b(${decimalInteger})\\b((${frac})\\b|\\.)?|(${frac})\\b` }, // DecimalBigIntegerLiteral { begin: `\\b(0|[1-9](_?[0-9])*)n\\b` }, // NonDecimalIntegerLiteral { begin: "\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*n?\\b" }, { begin: "\\b0[bB][0-1](_?[0-1])*n?\\b" }, { begin: "\\b0[oO][0-7](_?[0-7])*n?\\b" }, // LegacyOctalIntegerLiteral (does not include underscore separators) // https://tc39.es/ecma262/#sec-additional-syntax-numeric-literals { begin: "\\b0[0-7]+n?\\b" }, ], relevance: 0 }; const SUBST = { className: 'subst', begin: '\\$\\{', end: '\\}', keywords: KEYWORDS, contains: [] // defined later }; const HTML_TEMPLATE = { begin: '\.?html`', end: '', starts: { end: '`', returnEnd: false, contains: [ hljs.BACKSLASH_ESCAPE, SUBST ], subLanguage: 'xml' } }; const CSS_TEMPLATE = { begin: '\.?css`', end: '', starts: { end: '`', returnEnd: false, contains: [ hljs.BACKSLASH_ESCAPE, SUBST ], subLanguage: 'css' } }; const GRAPHQL_TEMPLATE = { begin: '\.?gql`', end: '', starts: { end: '`', returnEnd: false, contains: [ hljs.BACKSLASH_ESCAPE, SUBST ], subLanguage: 'graphql' } }; const TEMPLATE_STRING = { className: 'string', begin: '`', end: '`', contains: [ hljs.BACKSLASH_ESCAPE, SUBST ] }; const JSDOC_COMMENT = hljs.COMMENT( /\/\*\*(?!\/)/, '\\*/', { relevance: 0, contains: [ { begin: '(?=@[A-Za-z]+)', relevance: 0, contains: [ { className: 'doctag', begin: '@[A-Za-z]+' }, { className: 'type', begin: '\\{', end: '\\}', excludeEnd: true, excludeBegin: true, relevance: 0 }, { className: 'variable', begin: IDENT_RE + '(?=\\s*(-)|$)', endsParent: true, relevance: 0 }, // eat spaces (not newlines) so we can find // types or variables { begin: /(?=[^\n])\s/, relevance: 0 } ] } ] } ); const COMMENT = { className: "comment", variants: [ JSDOC_COMMENT, hljs.C_BLOCK_COMMENT_MODE, hljs.C_LINE_COMMENT_MODE ] }; const SUBST_INTERNALS = [ hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE, HTML_TEMPLATE, CSS_TEMPLATE, GRAPHQL_TEMPLATE, TEMPLATE_STRING, // Skip numbers when they are part of a variable name { match: /\$\d+/ }, NUMBER, // This is intentional: // See https://github.com/highlightjs/highlight.js/issues/3288 // hljs.REGEXP_MODE ]; SUBST.contains = SUBST_INTERNALS .concat({ // we need to pair up {} inside our subst to prevent // it from ending too early by matching another } begin: /\{/, end: /\}/, keywords: KEYWORDS, contains: [ "self" ].concat(SUBST_INTERNALS) }); const SUBST_AND_COMMENTS = [].concat(COMMENT, SUBST.contains); const PARAMS_CONTAINS = SUBST_AND_COMMENTS.concat([ // eat recursive parens in sub expressions { begin: /(\s*)\(/, end: /\)/, keywords: KEYWORDS, contains: ["self"].concat(SUBST_AND_COMMENTS) } ]); const PARAMS = { className: 'params', // convert this to negative lookbehind in v12 begin: /(\s*)\(/, // to match the parms with end: /\)/, excludeBegin: true, excludeEnd: true, keywords: KEYWORDS, contains: PARAMS_CONTAINS }; // ES6 classes const CLASS_OR_EXTENDS = { variants: [ // class Car extends vehicle { match: [ /class/, /\s+/, IDENT_RE, /\s+/, /extends/, /\s+/, regex.concat(IDENT_RE, "(", regex.concat(/\./, IDENT_RE), ")*") ], scope: { 1: "keyword", 3: "title.class", 5: "keyword", 7: "title.class.inherited" } }, // class Car { match: [ /class/, /\s+/, IDENT_RE ], scope: { 1: "keyword", 3: "title.class" } }, ] }; const CLASS_REFERENCE = { relevance: 0, match: regex.either( // Hard coded exceptions /\bJSON/, // Float32Array, OutT /\b[A-Z][a-z]+([A-Z][a-z]*|\d)*/, // CSSFactory, CSSFactoryT /\b[A-Z]{2,}([A-Z][a-z]+|\d)+([A-Z][a-z]*)*/, // FPs, FPsT /\b[A-Z]{2,}[a-z]+([A-Z][a-z]+|\d)*([A-Z][a-z]*)*/, // P // single letters are not highlighted // BLAH // this will be flagged as a UPPER_CASE_CONSTANT instead ), className: "title.class", keywords: { _: [ // se we still get relevance credit for JS library classes ...ECMAScript.TYPES, ...ECMAScript.ERROR_TYPES ] } }; const USE_STRICT = { label: "use_strict", className: 'meta', relevance: 10, begin: /^\s*['"]use (strict|asm)['"]/ }; const FUNCTION_DEFINITION = { variants: [ { match: [ /function/, /\s+/, IDENT_RE, /(?=\s*\()/ ] }, // anonymous function { match: [ /function/, /\s*(?=\()/ ] } ], className: { 1: "keyword", 3: "title.function" }, label: "func.def", contains: [ PARAMS ], illegal: /%/ }; const UPPER_CASE_CONSTANT = { relevance: 0, match: /\b[A-Z][A-Z_0-9]+\b/, className: "variable.constant" }; function noneOf(list) { return regex.concat("(?!", list.join("|"), ")"); } const FUNCTION_CALL = { match: regex.concat( /\b/, noneOf([ ...ECMAScript.BUILT_IN_GLOBALS, "super", "import" ].map(x => `${x}\\s*\\(`)), IDENT_RE, regex.lookahead(/\s*\(/)), className: "title.function", relevance: 0 }; const PROPERTY_ACCESS = { begin: regex.concat(/\./, regex.lookahead( regex.concat(IDENT_RE, /(?![0-9A-Za-z$_(])/) )), end: IDENT_RE, excludeBegin: true, keywords: "prototype", className: "property", relevance: 0 }; const GETTER_OR_SETTER = { match: [ /get|set/, /\s+/, IDENT_RE, /(?=\()/ ], className: { 1: "keyword", 3: "title.function" }, contains: [ { // eat to avoid empty params begin: /\(\)/ }, PARAMS ] }; const FUNC_LEAD_IN_RE = '(\\(' + '[^()]*(\\(' + '[^()]*(\\(' + '[^()]*' + '\\)[^()]*)*' + '\\)[^()]*)*' + '\\)|' + hljs.UNDERSCORE_IDENT_RE + ')\\s*=>'; const FUNCTION_VARIABLE = { match: [ /const|var|let/, /\s+/, IDENT_RE, /\s*/, /=\s*/, /(async\s*)?/, // async is optional regex.lookahead(FUNC_LEAD_IN_RE) ], keywords: "async", className: { 1: "keyword", 3: "title.function" }, contains: [ PARAMS ] }; return { name: 'JavaScript', aliases: ['js', 'jsx', 'mjs', 'cjs'], keywords: KEYWORDS, // this will be extended by TypeScript exports: { PARAMS_CONTAINS, CLASS_REFERENCE }, illegal: /#(?![$_A-z])/, contains: [ hljs.SHEBANG({ label: "shebang", binary: "node", relevance: 5 }), USE_STRICT, hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE, HTML_TEMPLATE, CSS_TEMPLATE, GRAPHQL_TEMPLATE, TEMPLATE_STRING, COMMENT, // Skip numbers when they are part of a variable name { match: /\$\d+/ }, NUMBER, CLASS_REFERENCE, { className: 'attr', begin: IDENT_RE + regex.lookahead(':'), relevance: 0 }, FUNCTION_VARIABLE, { // "value" container begin: '(' + hljs.RE_STARTERS_RE + '|\\b(case|return|throw)\\b)\\s*', keywords: 'return throw case', relevance: 0, contains: [ COMMENT, hljs.REGEXP_MODE, { className: 'function', // we have to count the parens to make sure we actually have the // correct bounding ( ) before the =>. There could be any number of // sub-expressions inside also surrounded by parens. begin: FUNC_LEAD_IN_RE, returnBegin: true, end: '\\s*=>', contains: [ { className: 'params', variants: [ { begin: hljs.UNDERSCORE_IDENT_RE, relevance: 0 }, { className: null, begin: /\(\s*\)/, skip: true }, { begin: /(\s*)\(/, end: /\)/, excludeBegin: true, excludeEnd: true, keywords: KEYWORDS, contains: PARAMS_CONTAINS } ] } ] }, { // could be a comma delimited list of params to a function call begin: /,/, relevance: 0 }, { match: /\s+/, relevance: 0 }, { // JSX variants: [ { begin: FRAGMENT.begin, end: FRAGMENT.end }, { match: XML_SELF_CLOSING }, { begin: XML_TAG.begin, // we carefully check the opening tag to see if it truly // is a tag and not a false positive 'on:begin': XML_TAG.isTrulyOpeningTag, end: XML_TAG.end } ], subLanguage: 'xml', contains: [ { begin: XML_TAG.begin, end: XML_TAG.end, skip: true, contains: ['self'] } ] } ], }, FUNCTION_DEFINITION, { // prevent this from getting swallowed up by function // since they appear "function like" beginKeywords: "while if switch catch for" }, { // we have to count the parens to make sure we actually have the correct // bounding ( ). There could be any number of sub-expressions inside // also surrounded by parens. begin: '\\b(?!function)' + hljs.UNDERSCORE_IDENT_RE + '\\(' + // first parens '[^()]*(\\(' + '[^()]*(\\(' + '[^()]*' + '\\)[^()]*)*' + '\\)[^()]*)*' + '\\)\\s*\\{', // end parens returnBegin:true, label: "func.def", contains: [ PARAMS, hljs.inherit(hljs.TITLE_MODE, { begin: IDENT_RE, className: "title.function" }) ] }, // catch ... so it won't trigger the property rule below { match: /\.\.\./, relevance: 0 }, PROPERTY_ACCESS, // hack: prevents detection of keywords in some circumstances // .keyword() // $keyword = x { match: '\\$' + IDENT_RE, relevance: 0 }, { match: [ /\bconstructor(?=\s*\()/ ], className: { 1: "title.function" }, contains: [ PARAMS ] }, FUNCTION_CALL, UPPER_CASE_CONSTANT, CLASS_OR_EXTENDS, GETTER_OR_SETTER, { match: /\$[(.]/ // relevance booster for a pattern common to JS libs: `$(something)` and `$.something` } ] }; }