highlightjs-apex
Version:
Apex Language of Salesforce highlighting plugin for highlight.js
1,471 lines (1,397 loc) • 33.3 kB
JavaScript
/*
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
}; */
}