UNPKG

highlightjs-apex

Version:

Apex Language of Salesforce highlighting plugin for highlight.js

1,471 lines (1,397 loc) 33.3 kB
/* Language: Apex Author: David Schach <dschach@x2od.com> Category: Salesforce, Force.com, Salesforce Platform, Einstein 1 Platform Website: https://developer.salesforce.com/ */ /** @type LanguageFn */ export default function (hljs) { const regex = hljs.regex; const APEX_IDENT_RE = '[a-zA-Z][a-zA-Z_0-9]*'; //const METHOD_RE = '[a-z][a-zA-Z_0-9]*'; const APEX_OPT_RE = '[a-zA-Z_0-9]*'; const APEX_IDENT_WORD_RE = '\\b' + APEX_IDENT_RE + '\\b'; const ANNOTATION_RE = '@' + APEX_IDENT_RE; const SPACEPARENS_LOOKAHEAD = /(?=\s*\()/; //const PARENS_LOOKAHEAD = /(?=\()/; const SPACE = /\s+/; const OPENCURLY = /\{/; const CLOSECURLY = /\s*\}/; const OPENPARENS = /\(/; const CLOSEPARENS = /\)/; //const CURLYORNEWLINE = /\s*(?=\}|\n|$)/; const COMMENT_LEADER = { match: /^\s+\*\s+/, scope: 'comment', relevance: 0 }; const URL_RE = /(https?):\/\/[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]/; const ACCESSOR = /(?<!\?)\./; const NULLSAFE = /\?\./; const DOT_NOTATION = [ { match: ACCESSOR, scope: 'punctuation', relevance: 0 }, { match: NULLSAFE, scope: 'operator', relevance: 0 } ]; const MM_DD_YYYY = /\d{1,2}\/\d{1,2}\/\d{4}/; const YYYY_MM_DD = /\d{4}-\d{1,2}-\d{1,2}/; const SALESFORCE_ID = { match: /(?<!\.)\bId\b/, scope: 'type', relevance: 8 }; /** * @param {...(RegExp | string) } args * @returns {string} */ function noneOf(...list) { return regex.concat('(?!', list.join('|'), ')'); } /* LISTS */ const NUMBERS = { scope: 'number', match: regex.either( /\b(\d{4}\-\d{2}\-\d{2}T\d{2}\:\d{2}\:\d{2}(\.\d{1,3})?(\-|\+)\d{2}\:\d{2})\b/, //datetime /\b(\d{4}\-\d{2}\-\d{2}T\d{2}\:\d{2}\:\d{2}(\.\d{1,3})?(Z)?)\b/, //datetime /\b(\d{4}\-\d{2}\-\d{2})\b/, //date /\b0(x|X)[0-9a-fA-F_]+(U|u|L|l|UL|Ul|uL|ul|LU|Lu|lU|lu)?\b/, //hex /\b0(b|B)[01_]+(U|u|L|l|UL|Ul|uL|ul|LU|Lu|lU|lu)?\b/, //binary /\b([0-9]+)?\.[0-9]+((e|E)[0-9]+)?(F|f|D|d|M|m)?\b/, //decimal /(-?)\b[0-9]+(e|E)[0-9]+(F|f|D|d|M|m)?\b/, //decimal /(-?)\b[0-9]+(F|f|D|d|M|m)\b/, //decimal /(-?)\b[0-9]+(U|u|L|l|UL|Ul|uL|ul|LU|Lu|lU|lu)?\b/, //decimal /(-?)(\b0[0-9]+|(\b\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?)/ // C_NUMBER_MODE ), relevance: 0 }; // keyword const KEYWORD_LIST = [ 'try', 'catch', 'finally', 'get', 'set', 'put', 'if', 'for', 'else', 'do', 'while', 'continue', 'break', 'implements', 'extends', 'return', 'throw', 'when', 'new' ]; const LANGUAGE_VAR_LIST = ['instanceof', 'super', 'this']; // keyword const ACCESS_MODIFIER_LIST = [ 'abstract', 'final', 'global', 'override', 'private', 'protected', 'public', 'static', 'testMethod', 'transient', 'virtual', 'webservice' ]; const TYPES = [ 'anytype|0', 'blob|0', 'boolean|0', 'byte|0', 'currency|0', 'date|0', 'datetime|0', 'decimal|0', 'double|0', 'enum|0', 'float|0', 'integer|0', 'long|0', 'object|0', 'pagereference|8', 'selectoption|8', 'short|0', 'sobject|10', 'string|0', 'time|0', 'void|0', 'float|0' ]; const DMLS = [ 'insert', 'update', 'upsert', 'delete', 'undelete', 'merge', 'convertLead' ]; const NAMESPACE_LIST = [ //'ApexPages', // also a System class 'AppLauncher', 'Approval', 'Auth', 'Cache', 'Canvas', 'ChatterAnswers', 'CommerceOrders', 'fsccashflow', 'CommercePayments', 'ConnectApi', 'Context', 'Database', 'Datacloud', 'DataRetrieval', 'DataSource', 'Dataweave', 'Dom', 'EventBus', 'ExternalService', 'Flow', 'FormulaEval', 'Functions', 'id_verification', 'industriesDigitalLending', 'Invocable', 'KbManagement', 'LxScheduler', 'Messaging', 'Metadata', 'Pref_center', 'Process', 'QuickAction', 'Reports', 'RichMessageing', 'Schema', // also a System class 'Search', 'Sfc', 'Sfdc_Checkout', 'IndustriesNlpSvc', 'PlaceQuote', 'Sfdc_Enablement', 'Sfdc_surveys', 'Site', 'Slack|8', 'Support', 'System|0', 'TerritoryMgmt', 'TxnSecurity', 'UserProvisioning', 'VisualEditor', 'Wave' ]; const SYSTEM_CLASSES = [ //'AccessLevel', 'Address', 'Answers', 'ApexPages', 'Approval', 'Assert', 'AsyncInfo', 'AsyncOptions', 'BusinessHours', 'Cases', 'Collator', 'Compression', 'Continuation', 'Cookie', 'Crypto', 'Database', 'Date', 'Datetime', 'Decimal', 'Dom', 'Domain', 'DomainCreator', 'DomainParser', 'EmailMessages', 'EncodingUtil', 'EventBus', 'Exception', 'FeatureManagement', 'FlexQueue', 'Formula', 'FormulaRecalcFieldError', 'FormulaRecalcResult', 'Http', 'HttpRequest', 'HttpResponse', 'Ideas', 'IsvPartners', 'JSON', 'JSONGenerator', 'JSONParser', 'Label', 'Limits', 'Location', 'Matcher', 'Math', 'Messaging', 'MultiStaticResourceCalloutMock', 'Network', 'OrgLimit', 'OrgLimits', 'Packaging', 'PageReference', 'Pattern', 'QueueableDuplicateSignature', 'QueueableDuplicateSignature.Builder', 'QuickAction', 'Request', 'ResetPasswordResult', 'RestContext', 'RestRequest', 'RestResponse', //'Schema', 'Search', 'Security', 'SelectOption|8', 'Site', 'SObject', 'SObjectAccessDecision', 'StaticResourceCalloutMock', 'Test', 'TimeZone', //'Trigger', // handled separately 'Type', 'URL', 'UserInfo', 'UserManagement', 'UUID', 'Version', 'WebServiceCallout', 'XmlStreamReader', 'XmlStreamWriter' ]; const SYSTEM_ENUMS = [ 'AccessType', 'AccessLevel', 'CallbackStatus', 'DomainType', 'JSONToken', 'LoggingLevel', 'Quiddity|10', 'TriggerOperation', 'operationType' ]; const LITERALS_LIST = ['false', 'true', 'null']; //const LITERALS_MATCH = { match: [/(?<!\.)(?:false|true|null)\b/], scope: 'literal' }; const PUNCTUATION_LIST = [ ',', /;/, /(?<=\w)\>/, /\<(?=\w)/, /\{|\}/, /\(|\)/, /\{|\}/ ]; const PUNCTUATION_COMMA = { match: /,/, scope: 'punctuation', relevance: 0 }; const OPERATORS_LIST = [ /--/, // decrement /\+\+/, // increment /\&\&|\|\|/, // logical /\*\=|\/\=|\%\=|\+\=|-\=/, // assignment.compound /\&\=|\^\=|<<\=|>>\=|>>>\=|\|\=/, // assignment.compound.bitwise /\&|~|\^|\|/, // bitwise /<<|>>/, // bitwise.shift /<\=|>\=|\s(<|>)\s/, // relational /\=\=|!\=/, // comparison /\=>/, // map assign /!(?=\w)/, // negator /(?<=\s)(\?|:)(?=\s)/, // standalone ? or : (ternary operator?) NULLSAFE, // null-safe operator /(?<=\s)(\?\?)(?=\s)/, // null coalescing operator /(?<!\?)\?(?!\?|\.|\[)/, // ternary operator or CONDITIONAL_OPERATOR /%[^%]|(?<!\n\s+)\*[^\/]|\/[^\/\*]|(?<!\-)\-(?!\-)|(?<!\+)\+(?!\+)/, // arithmetic /(?<!\=|!)\=(?!\=|>)/ // assignment ]; const BUILT_INS = NAMESPACE_LIST.concat(...SYSTEM_CLASSES); const KEYWORDS = { $pattern: regex.concat(/(?<!\.)/, APEX_IDENT_WORD_RE), // /(?!\s*\()/), keyword: [...KEYWORD_LIST, ...ACCESS_MODIFIER_LIST, ...DMLS], // 'variable.language': LANGUAGE_VAR_LIST, // built_in: BUILT_INS, // handled in NAMESPACES array type: TYPES, literal: LITERALS_LIST }; const RESERVED_WORDS = [ ...LITERALS_LIST, ...KEYWORD_LIST, ...ACCESS_MODIFIER_LIST, ...NAMESPACE_LIST, ...SYSTEM_CLASSES, ...SYSTEM_ENUMS, ...LANGUAGE_VAR_LIST, ...DMLS ]; const LANGUAGE_VARS_RE = { match: regex.concat(/\b/, regex.either(...LANGUAGE_VAR_LIST), /\b/), scope: 'variable.language', relevance: 0 }; // put this section early so we eat up the 'exception' method calls. All the rest are regular ones. const NAMESPACES = [ { match: [ regex.concat(/\b/, regex.either(...SYSTEM_ENUMS)), /\./, APEX_IDENT_RE, /\b\s*(?![\.\(])/ ], // TODO: Find a better scope for the enum value scope: { 1: 'built_in', 2: 'punctuation', 3: 'variable' }, relevance: 0 }, { match: [ regex.concat(/\b/, regex.either(...SYSTEM_CLASSES)), /\./, APEX_IDENT_RE, /\b\s*(?![\.\(])/ ], scope: { 1: 'built_in', 2: 'punctuation', 3: 'type' }, relevance: 0 }, { match: [ regex.concat(/(?<!\.)/, regex.either(...NAMESPACE_LIST)), /\./, APEX_IDENT_WORD_RE, /\b(?=\.)/ ], scope: { 1: 'built_in', 2: 'punctuation', 3: 'type' } }, { match: [ regex.concat(/\b/, regex.either(...SYSTEM_CLASSES), /\b/), /\./, APEX_IDENT_WORD_RE, SPACEPARENS_LOOKAHEAD ], scope: { 1: 'built_in', 2: 'punctuation', 3: 'title.function.invoke' } }, { match: [ regex.concat(/\b/, regex.either(...NAMESPACE_LIST, ...SYSTEM_CLASSES)), /\./, APEX_IDENT_WORD_RE, /\b(?!\.)/ ], scope: { 1: 'built_in', 2: 'punctuation', 3: 'keyword' } }, { // Trigger variables match: [ /\bTrigger\b/, /\./, /(isExecuting|isInsert|isUpdate|isDelete|isBefore|isAfter|isUndelete|new|newMap|old|oldMap|size|operationType)\b(?!\.)/ ], scope: { 1: 'built_in', 2: 'punctuation', 3: 'type' }, relevance: 10 }, { // Trigger operationtype match: [ /\btrigger\b/, /\./, 'operationtype', /\./, regex.concat(APEX_IDENT_WORD_RE, /\b/) ], scope: { 1: 'built_in', 2: 'punctuation', 3: 'keyword', 4: 'punctuation', 5: 'variable.constant' }, relevance: 10 } ]; // use this to include comma // use comma only for situations where parens will break things const PUNCTUATION = [ { match: regex.either(...PUNCTUATION_LIST), scope: 'punctuation', relevance: 0 }, PUNCTUATION_COMMA ]; const STRINGS = hljs.inherit(hljs.APOS_STRING_MODE, { scope: 'string', relevance: 0, contains: [{ match: /\\'/, scope: 'literal', relevance: 0 }] }); const COMMENT_STRINGS = [ { begin: '`', end: '`', scope: 'string' }, { begin: /'/, end: /'/, scope: 'string' }, { begin: /"/, end: /"/, scope: 'string' } ]; const COMMENT_LINE = hljs.COMMENT('//', /[$\n]/, { relevance: 0 }); //const APEX_DOX_LINK = /(?<=\{\s*)@link\s+/; const APEX_DOX_SEE = /(?<!\{\s*)@see\s+/; //const APEX_DOX_LINKSEE = regex.either(APEX_DOX_LINK, APEX_DOX_SEE); const APEX_DOX_OVERLOAD = /(\[\d+\])*/; const APEXDOC_LINKSEE = /(?:@link|@see)\s+/; const APEXDOC_DIVIDER = regex.either('#', ACCESSOR); // TODO: Rebuild ApexDoc for Summer 26 relesae /* const APEX_CLASS_RE = '[A-Z][a-zA-Z_0-9]*'; const APEX_METHOD_RE = '[a-z][a-zA-Z_0-9]*'; const APEXDOC_REFERENCE = [ // Class#member(parameters) { }, // member(parameters) { }, // Class#member // Class.member { }, // Class#Class {}, // class.class#.member { }, // class { }, // method( { }, // string { }, // url { }, // markdown ]; */ const APEX_DOX = [ // https://github.com/no-stack-dub-sack/apexdox-vs-code // TODO: Merge ApexDox and ApexDoc for Summer 26 /* { // Markdown URL begin: /@(see|author)\s+(?=\[)/, beginScope: 'doctag', starts: { end: /\n/, returnEnd: true, subLanguage: ['markdown', 'html'] } }, */ { // Class.InnerClass.InnerClassMethod match: [ /\{*/, APEXDOC_LINKSEE, regex.concat( APEX_IDENT_RE, ACCESSOR, APEX_IDENT_RE, ACCESSOR, APEX_IDENT_RE, APEX_DOX_OVERLOAD ), /(?=[\}\s])/ ], scope: { 2: 'doctag', 3: 'title.function' } }, { // TODO: Separate it out and handle class.class.method, and class#method // @see Class.InnerClassMethod match: [ APEX_DOX_SEE, regex.concat( APEX_IDENT_RE, APEXDOC_DIVIDER, APEX_IDENT_RE, APEX_DOX_OVERLOAD, /(?=\s)/ ) ], scope: { 1: 'doctag', 2: 'title.function' } }, { // URL match: [/(?<!\{\s*)@see\s+/, URL_RE], scope: { 1: 'doctag', 2: 'link' } }, { // Class match: [/(?<!\{\s*)@see\s+/, regex.concat(APEX_IDENT_WORD_RE, /(?=\s)/)], scope: { 1: 'doctag', 2: 'title.class' } }, { begin: /@example(?!\s+\*\s+\{)/, beginScope: 'doctag', end: /(@|\*\/)/, returnEnd: true, starts: { endsWithParent: true, subLanguage: ['apex', 'xml', 'javascript'] } } ]; const OFFICIAL_APEXDOC = [ { // {@link class#method(parameters)} // {@link #method(parameters)} // @see class#method(parameters) // @see #method(parameters) begin: [ /\{*/, APEXDOC_LINKSEE, APEX_OPT_RE, regex.concat(APEXDOC_DIVIDER, APEX_IDENT_RE), /\s*\(/ ], beginScope: { 2: 'doctag', 3: 'title.class', 4: 'title.function' //5: 'punctuation' }, end: CLOSEPARENS, //endScope: 'punctuation', contains: [ COMMENT_LEADER, { match: [SPACE, APEX_IDENT_RE, /\s*(?=[,)])/], scope: { 2: 'variable' } }, { match: [APEX_IDENT_RE, /(?=\s)/], scope: { 1: 'type' } }, { match: /,/, scope: 'punctuation' }, COMMENT_LINE, COMMENT_LEADER ] }, { // {@link class#member} // {@link #member} match: [ /\s/, APEXDOC_LINKSEE, regex.concat(APEX_OPT_RE, APEXDOC_DIVIDER, APEX_IDENT_RE), //\s*/, /(?=[\}\s])/ ], scope: { 2: 'doctag', 3: 'title.function' } }, { // {@link class.method} match: [ OPENCURLY, APEXDOC_LINKSEE, regex.concat(APEX_IDENT_RE, APEXDOC_DIVIDER, APEX_IDENT_RE), CLOSECURLY ], scope: { 2: 'doctag', 3: 'title.function' } }, { // {@link class} match: [OPENCURLY, APEXDOC_LINKSEE, APEX_IDENT_RE, CLOSECURLY], scope: { 2: 'doctag', 3: 'title.class' } }, { // @see "text-string" begin: [OPENCURLY, APEXDOC_LINKSEE, /(?=")/], beginScope: { 2: 'doctag' }, end: CLOSECURLY, contains: COMMENT_STRINGS }, { begin: [OPENCURLY, APEXDOC_LINKSEE], beginScope: { 2: 'doctag' }, end: CLOSECURLY, returnEnd: true, subLanguage: ['markdown', 'xml'], contains: [ { // URL match: URL_RE, scope: 'link' } ] } ]; const COMMENT_BLOCK = hljs.COMMENT('/\\*', '\\*/', { relevance: 0, contains: [ { // Use up <br> tags in ApexDox match: '<br>', relevance: 0 }, { // eat up @'s in emails to prevent them being recognized as doctags begin: /\w+@/, relevance: 0 }, /* { match: [ /@(?:exception|throws)/, SPACE, /\S+\b/ //regex.concat(APEX_IDENT_RE, ACCESSOR, APEX_IDENT_RE) ], scope: { 1: 'doctag', 3: 'title.class' }, relevance: 0 }, */ { match: [/@(?:exception|throws)/, SPACE, APEX_IDENT_RE], scope: { 1: 'doctag', 3: 'title.class' }, relevance: 0 }, /******* LINK and SEE ***********/ APEX_DOX, OFFICIAL_APEXDOC, { // Handle `@param` to highlight the parameter name following // after. scope: 'doctag', begin: '@param', end: /$/, contains: [ { scope: 'variable', variants: [{ match: APEX_IDENT_RE }], endsParent: true } ] }, { // various strings variants: COMMENT_STRINGS, contains: [hljs.BACKSLASH_ESCAPE], relevance: 0 }, { // version numbers begin: /[0-9]+\.[0-9]+/, end: /\.*\s+/, scope: 'number', excludeEnd: true }, { // #YYYY-MM-DD# (ISO-Date) or #M/D/YYYY# (US/Euro-Date) begin: regex.either(YYYY_MM_DD, MM_DD_YYYY), scope: 'literal' }, { begin: [OPENCURLY, '@code'], beginScope: { 2: 'doctag' }, starts: { end: CLOSECURLY, //endScope: 'punctuation', contains: [ COMMENT_LEADER, { begin: OPENCURLY, end: CLOSECURLY, skip: true } ], subLanguage: ['apex', 'xml', 'javascript'] } }, { // any other doctag with a curly brace begin: [OPENCURLY, ANNOTATION_RE], beginScope: { 2: 'doctag' }, end: CLOSECURLY //endScope: 'punctuation' //contains: [{scope: 'code', endsWithParent: true}], //returnEnd: true, //scope: 'code' //subLanguage: ['apex', 'xml', 'javascript', 'bash'] }, { // remaining doctags match: ANNOTATION_RE, scope: 'doctag', relevance: 0 }, COMMENT_LEADER ] }); const COMMENTS = [COMMENT_BLOCK, COMMENT_LINE, COMMENT_LEADER]; const OPERATORS = { match: regex.either(...OPERATORS_LIST), scope: 'operator', relevance: 0 }; const COLLECTIONS = [ { //scope: 'clause: collection', begin: /\b(list|set|map)(?=\s*<)/, beginScope: 'type', end: /\>+/, endScope: 'punctuation', contains: [ { match: /\<|,/, scope: 'punctuation' }, { match: APEX_IDENT_WORD_RE, scope: 'type' } ], relevance: 8 }, { // array reference (only has integer in array) match: [ regex.optional(APEX_IDENT_WORD_RE), /\[/, regex.optional(/\d+/), /\]/ ], scope: { 1: 'variable', 2: 'punctuation', 3: 'number', 4: 'punctuation' } }, { // type[] var = new type[]{values} match: [APEX_IDENT_RE, /\[\]/], // array notation scope: { 1: 'type', 2: 'punctuation' }, relevance: 0 } ]; const ANNOTATIONS = [ { //Type 1: one annotation // @isTest match: regex.concat(ANNOTATION_RE, /\b(?!\s*\()/), scope: 'meta' }, { // Type 2: annotation and parentheses // @SuppressWarnings('PMD.AvoidGlobalModifier')) // Type 3: annotation on one line and parentheses on next // @IsTest // (Seealldata=true) scope: 'meta', begin: [regex.concat(ANNOTATION_RE, /\b/), /\s*/, OPENPARENS], beginScope: { 3: 'punctuation' }, end: CLOSEPARENS, endScope: 'punctuation', //scope: 'clause: annotation', contains: [ { match: [APEX_IDENT_WORD_RE, /\s*=/], scope: { 1: 'keyword', 2: 'operator' }, contains: [OPERATORS] }, STRINGS, NUMBERS ], keywords: { literal: LITERALS_LIST } } ]; const EXCEPTION = { // Various Apex Exception types match: [/\b[a-zA-Z0-9\.]*Exception/, SPACE, APEX_IDENT_RE], scope: { 1: 'type', 3: 'variable' }, relevance: 0 }; const VAR_ASSIGN = [ { // mynum = // could also be a collection variable match: [/(?<!\.)/, APEX_IDENT_WORD_RE, /\s*(?=\=[^\>])/], scope: { 2: 'variable' }, relevance: 0 }, { // MyType mynum = match: [APEX_IDENT_WORD_RE, '(?=\\s+' + APEX_IDENT_RE + /\s*\=/ + ')'], scope: { 1: 'type' }, relevance: 0 }, { match: [APEX_IDENT_WORD_RE, SPACE, APEX_IDENT_WORD_RE, /\s*(?=\=[^\>])/], scope: { 1: 'type', 3: 'variable' }, relevance: 0 } ]; const CASTING = { // Casting match: [/(?<=\=\s*\()/, APEX_IDENT_RE, '(?=\\)\\s*' + APEX_IDENT_RE + ')'], scope: { 2: 'type' }, relevance: 0 }; const INSTANTIATE_TYPE = { // Account a = new Account(Name = 'test account); match: [/\bnew\s+/, APEX_IDENT_RE, SPACEPARENS_LOOKAHEAD], scope: { 2: 'type' }, relevance: 0 }; const PARAMS_CALL = { begin: OPENPARENS, beginScope: 'punctuation', end: CLOSEPARENS, endScope: 'punctuation', relevance: 0, keywords: KEYWORDS, contains: [] }; const METHOD_CALL = [ { match: [/(?<=\.)/, APEX_IDENT_RE, SPACEPARENS_LOOKAHEAD], scope: { 2: 'title.function.invoke' }, relevance: 1, contains: [...DOT_NOTATION], starts: PARAMS_CALL }, { match: [ /^\s*/, noneOf(...KEYWORD_LIST), APEX_IDENT_RE, SPACEPARENS_LOOKAHEAD ], scope: { 3: 'title.function.invoke' }, relevance: 1, starts: PARAMS_CALL } ]; PARAMS_CALL.contains = [ STRINGS, INSTANTIATE_TYPE, COMMENTS, OPERATORS, COLLECTIONS, NAMESPACES, NUMBERS, SALESFORCE_ID, METHOD_CALL, { keywords: { KEYWORDS }, // mymethod(var1, var2); comma-separated list // must be followed by comma or paren match: regex.concat( /(?<=\s|\(|,)/, noneOf(...LITERALS_LIST), APEX_IDENT_RE, /\b/, /(?!\.)/ ), scope: 'variable', relevance: 0 }, { match: /\(|,|\./, scope: 'punctuation', relevance: 0 }, { match: APEX_IDENT_RE, scope: 'variable' } ]; const PARAMS_DECLARATION = { scope: 'params', // NOTE: declaration // no begin because only started from method declaration end: CLOSEPARENS, endScope: 'punctuation', relevance: 1, keywords: KEYWORDS, contains: [ NUMBERS, STRINGS, COMMENTS, COLLECTIONS, ...NAMESPACES, { match: /,|\(/, scope: 'punctuation' }, SALESFORCE_ID, { match: [/(?<=\(|,)\s*/, APEX_IDENT_RE, /(?=\s)/], scope: { 2: 'type' } }, { // mymethod(Date myDate, Date yourDate); highlights second part of each parameter // must be followed by comma or paren match: [SPACE, APEX_IDENT_RE, /\s*(?=[,)])/], scope: { 2: 'variable' } } ] }; const EXTEND_IMPLEMENT = { returnEnd: true, endsWithParent: true, beginKeywords: 'implements extends', end: OPENCURLY, contains: [ { match: [APEX_IDENT_WORD_RE, /(?=\.)/], scope: { 1: 'built_in' } }, { match: regex.concat(APEX_IDENT_WORD_RE, /(?=\>)/), scope: 'type' }, { match: APEX_IDENT_WORD_RE, scope: 'title.class.inherited' }, { match: /<|>|,/, scope: 'punctuation' }, NAMESPACES ], relevance: 0 }; const TRIGGER_DECLARATION = { // trigger declaration begin: [ /\btrigger/, SPACE, APEX_IDENT_RE, SPACE, 'on', SPACE, APEX_IDENT_RE ], beginScope: { 1: 'keyword', 3: 'title.class', 5: 'operator', 7: 'type' }, end: /(?=\{)/, returnEnd: true, //scope: 'clause: trigger_declaration', contains: [ COMMENTS, { begin: OPENPARENS, end: CLOSEPARENS, contains: [ { match: /\b(before|after)\s+(insert|update|delete|merge|undelete)\b/, scope: 'keyword', relevance: 5 }, PUNCTUATION_COMMA ] } ] }; const CLASS_SHARING = { // class sharing relevance: 5, match: /\b(with|without|inherited)\s+sharing\b/, scope: 'keyword' }; const CLASS_DECLARATION = { // class declaration begin: [/(?!\.)/, /\b(class|interface)\b(?!\?|\.)/], beginScope: { 2: 'keyword' }, end: /(?=\{)/, relevance: 1, keywords: { type: TYPES, keyword: KEYWORD_LIST }, contains: [ { match: [/(?<=\bclass)\s+/, APEX_IDENT_RE], scope: { 2: 'title.class' } }, EXTEND_IMPLEMENT ] }; const ENUM_DECLARATION = { // enum declaration begin: [/\benum\s+/, APEX_IDENT_RE, /\s*\{/], beginScope: { 2: 'type', 3: 'punctuation' }, end: CLOSECURLY, endScope: 'punctuation', relevance: 0, contains: [ COMMENTS, PUNCTUATION_COMMA, { match: regex.concat(APEX_IDENT_WORD_RE), scope: 'variable.constant' } ] }; const DECLARATIONS = [ TRIGGER_DECLARATION, CLASS_SHARING, CLASS_DECLARATION, ENUM_DECLARATION, { // * Constructor // Matches public/private/protected methodname parens match: [ /(public|private|protected)\s+/, APEX_IDENT_RE, SPACEPARENS_LOOKAHEAD ], scope: { 1: 'keyword', 2: 'title.function' }, starts: PARAMS_DECLARATION, relevance: 1 }, { // * method declaration match: [ /(?<!new)(?<!return)(?<=\<|\>|\w)\s+/, APEX_IDENT_RE, SPACEPARENS_LOOKAHEAD ], scope: { 2: 'title.function' }, relevance: 1, starts: PARAMS_DECLARATION } ]; const SWITCH_STATEMENT = { match: [/\bswitch\s+on\s+/, APEX_IDENT_RE], scope: { 1: 'keyword', 2: 'variable' } }; const DML_OPERATIONS = [ { match: /as\s+(user|system)\b/, scope: 'keyword', relevance: 5 } ]; /** * SOQL SECTION */ const SOQL_KEYWORDS = [ // * orange italic 'ABOVE_OR_BELOW', 'ABOVE', 'ACTIVE', 'ADVANCED', 'ALL', 'ANY', 'ARRAY', 'AS', 'ASC', 'BELOW', 'CATEGORY', 'CONTAINS', 'CUSTOM', 'DATA', 'DESC', 'DIVISION', 'ELSE', 'END', 'FIND', 'FROM', 'METADATA', 'NETWORK', 'ON', 'PricebookId', 'RETURNING', 'ROLLUP', 'ROWS', 'SEARCH', 'SELECT', 'SNIPPET', 'SORT', 'SPELL_CORRECTION', 'STANDARD', 'THEN', 'USING', 'WHEN', 'WHERE', 'WITH', 'SCOPE', 'Delegated', 'Everything', 'Mine', 'My_Territory', 'My_Team_Territory', 'Team' /* /USING\s+SCOPE\s*(Delegated|Everything|Mine|My_Territory|My_Team_Territory|Team)/, */ ]; const SOQL_OPERATORS = [ // * orange italic 'AND', 'AT', 'FIRST', 'FOR', 'HAVING', 'IN', 'LAST', 'LIKE', 'LIMIT', 'LISTVIEW', 'NOT', 'NULLS', 'OFFSET', 'OR', 'REFERENCE', 'TRACKING', 'TYPEOF', 'UPDATE', 'VIEW', 'VIEWSTAT' ]; const SOQL_FUNCTIONS = [ // * blue italic 'AVG', 'convertCurrency', 'convertTimezone', 'COUNT_DISTINCT', 'COUNT', 'DISTANCE', 'EXCLUDES', 'FIELDS', 'FORMAT', 'GEOLOCATION', 'GROUPING', 'ROLLUP', 'CUBE', 'INCLUDES', 'MAX', 'MIN', 'SUM', 'toLabel', 'USER_MODE', 'SYSTEM_MODE', 'SECURITY_ENFORCED' ]; const SOQL_DATE_SELECT_FUNCTIONS = [ 'CALENDAR_MONTH', 'CALENDAR_QUARTER', 'CALENDAR_YEAR', 'DAY_IN_MONTH', 'DAY_IN_WEEK', 'DAY_IN_YEAR', 'DAY_ONLY', 'FISCAL_MONTH', 'FISCAL_QUARTER', 'FISCAL_YEAR', 'HOUR_IN_DAY', 'WEEK_IN_MONTH', 'WEEK_IN_YEAR' ]; const SOQL_DATE_LITERALS_SIMPLE = { match: /\b(TODAY|TOMORROW|YESTERDAY)\b/, scope: 'keyword' }; const SOQL_DATE_LITERALS_COMPLEX = { match: /(NEXT|LAST|THIS)_(90_DAY|DAY|FISCAL_QUARTER|FISCAL_YEAR|MONTH|QUARTER|WEEK|YEAR)S?\b/, scope: 'keyword', relevance: 8 }; const SOQL_DATE_LITERALS_W_PARAMS = { match: [ /(NEXT|LAST)_N_(DAY|FISCAL_QUARTER|FISCAL_YEAR|MONTH|QUARTER|WEEK|YEAR)S/, /\s*:\s*/, /\d+/ ], scope: { 1: 'keyword', 2: 'operator', 3: 'number' }, relevance: 8 }; const SOQL_DATE_LITERALS = [ SOQL_DATE_LITERALS_SIMPLE, SOQL_DATE_LITERALS_COMPLEX, SOQL_DATE_LITERALS_W_PARAMS ]; const SOQL_QUERY = { begin: [/\[/, /\s*(?=(SELECT|FIND)\b)/], end: /\]/, beginScope: { 1: 'punctuation' }, endScope: 'punctuation', scope: 'soql', relevance: 10, endsWithParent: true, keywords: { literal: KEYWORDS.literal, built_in: BUILT_INS }, contains: [ NUMBERS, OPERATORS, STRINGS, PUNCTUATION_COMMA, { match: /\(|\)/, scope: 'punctuation', relevance: 0 }, ...DOT_NOTATION, { begin: [/\bFROM\b/, SPACE], beginScope: { 1: 'keyword' }, //scope: 'clause: from_clause', end: /(?=\bWHERE\b|\]|\s|\))/, //returnEnd: true, contains: [{ match: APEX_IDENT_RE, scope: 'type' }, ...DOT_NOTATION] }, { match: regex.concat( /\b/, regex.either(...SOQL_DATE_SELECT_FUNCTIONS, ...SOQL_FUNCTIONS), /\b/ ), scope: 'title.function' }, { match: /\b(GROUP|ORDER)\s+BY\b/, scope: 'title.function' }, ...SOQL_DATE_LITERALS, { match: regex.concat( /\b/, regex.either( ...KEYWORDS.keyword, ...SOQL_KEYWORDS, ...SOQL_OPERATORS ), /\b/ ), scope: 'keyword' }, { // colon notation match: [/(?<=:)/, /\s*/, APEX_IDENT_WORD_RE, /(?!\()/], scope: { 3: 'variable' }, relevance: 0 }, { // any non-soql function used in the query match: [/(?<=:|\.)/, APEX_IDENT_RE, SPACEPARENS_LOOKAHEAD], scope: { 2: 'title.function.invoke' }, relevance: 0 }, //PARAMS_CALL, { match: /:/, scope: 'operator', relevance: 0 } ], illegal: '::' }; const FOR_LOOP = { match: [ /\bfor\b\s*/, OPENPARENS, APEX_IDENT_RE, SPACE, APEX_IDENT_RE, /\s*/, /:/ ], scope: { 3: 'type', 5: 'variable', 7: 'operator' } }; const THIS = { match: regex.concat('(?<=this.)', APEX_IDENT_RE, '(?!\\()'), scope: 'variable' }; const PROPERTY = { match: [ regex.concat(/\b/, noneOf(...RESERVED_WORDS)), APEX_IDENT_RE, /(?=\s*\{)/ ], scope: { 2: 'property' }, relevance: 0 }; const ILLEGALS = [ '</', '<#', '<]', '<div>', '<!--', '!DOCTYPE', /<iframe\b/, /^#/, /^import \.[a-zA-Z]+\./, /^import [\w]+/, /^import$/, /^include </, /^use\s+</, /\b(const|var)\s+\w+\s*=/, /\bstruct\b/, 'System.log', 'console.log', /\bfor\s+\w+\s+IN\s+/, /\bif\s+\w+\s+IN\s+/, /\bend\s+if\b/, /\bend\s+select\b/, /\b(int|var)\s+\w+\s+=/, /\b(int[0-9]+|bool)\b/, /\b\$/, /\b_/, // underscore at start of a word '::=', /\s#[a-zA-Z]/, /\s_[a-zA-Z]/, /\s\$[a-zA-Z]/, '#if', '%if', /\bif(?!\s+\()/, //coffeescript '%endif', '#endif', '#_', // clojure /\d\s+\d/, //clojure /\w::\w/, /\bfloat\b/, // many languages /(SELECT|RETURNING)\s+\*/, // SOQL requires explicit fields /END\s+LOOP/, /CREATE\s+FUNCTION/, /\bint\b/, /import\s+\w+\s+=\s+require\("\w+"\)/, '/^include\b/', /\buse\s+strict\b/, /\w+\s+=\s+"\S*";/, /\/include\//, /\bend(\.)?\n/, '"""', /\+\+\+/, /<%/, '<%#', '%%>', '<%%', ':-', /\bmergesort\(/, /\bvar\s+env\b/, /\bdef\b\s\W:/, /"[^"]+"/, // Quote_string_mode // /@\w+\[\w+\]/ //moonscript /\(\*|\*\)/ //mathematica, ocaml ]; const APEX_PARTS = [ COMMENTS, ANNOTATIONS, STRINGS, NUMBERS, CASTING, COLLECTIONS, DECLARATIONS, DML_OPERATIONS, EXCEPTION, FOR_LOOP, METHOD_CALL, INSTANTIATE_TYPE, LANGUAGE_VARS_RE, NAMESPACES, SALESFORCE_ID, SOQL_QUERY, SWITCH_STATEMENT, VAR_ASSIGN, OPERATORS, // includes null-safe PUNCTUATION, // includes comma DOT_NOTATION, THIS, PROPERTY //hljs.UPPER_CASE_CONSTANT // the last coloring to do is to eat the unclaimed variables /* { match: [regex.concat(/\b/, noneOf(...RESERVED_WORDS).concat([/\./]),/\b/), APEX_IDENT_WORD_RE, ';'], scope: {2: 'variable', 3: 'punctuation'}, keywords: KEYWORDS } */ ]; return { name: 'Apex', aliases: ['apex', 'lightning', 'soql', 'sosl'], case_insensitive: true, // language is case-insensitive disableAutodetect: false, ignoreIllegals: false, keywords: KEYWORDS, beginKeywords: 'Id', //TODO: Check this illegal: ILLEGALS, contains: [...APEX_PARTS] }; /* // * Can use later; omitted for now const CUSTOM_METADATA = { // Custom fields, types, etc. match: [ /(?<=[^\w\.])/, regex.concat( /\b/, APEX_IDENT_RE, /__(c|pc|r|b|e|mdt|x|share|kav|ka|history|del|s)/, /\b/ ), /(?=[\(\s;,])/ ], scope: { 2: 'type' }, relevance: 10 }; */ }