highlightjs-apex
Version:
Apex Language of Salesforce highlighting plugin for highlight.js
1,152 lines (1,097 loc) • 26.1 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 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 ACCESSOR = /(?<!\?)\./;
const NULLSAFE = /\?\./;
const DOT_NOTATION = [
{ match: ACCESSOR, scope: 'punctuation', relevance: 0 },
{ match: NULLSAFE, scope: 'operator', relevance: 0 }
];
/**
* @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',
'Support',
'System',
'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',
'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',
'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',
'TriggerOperation',
'operationType'
];
const LITERALS = ['false', 'true', 'null'];
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
/%[^%]|\*[^\/]|\/[^\/\*]|(?<!\-)\-(?!\-)|(?<!\+)\+(?!\+)/, // arithmetic
/(?<!\=|!)\=(?!\=|>)/ // assignment
];
const BUILT_INS = NAMESPACE_LIST.concat(...SYSTEM_CLASSES);
const KEYWORDS = {
$pattern: regex.concat(/(?<!\.)\b/, APEX_IDENT_RE, /\b/), // /(?!\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
};
const RESERVED_WORDS = [
...LITERALS,
...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(/\b/, regex.either(...NAMESPACE_LIST)),
/\./,
APEX_IDENT_WORD_RE,
/\b(?=\.)/
],
scope: { 1: 'built_in', 2: 'punctuation', 3: 'type' }
},
{
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_LINE = hljs.COMMENT('//', /[$\n]/, { relevance: 0 });
const COMMENT_BLOCK = hljs.COMMENT('/\\*', '\\*/', {
relevance: 0,
contains: [
{
// eat up @'s in emails to prevent them to be recognized as doctags
begin: /\w+@/,
relevance: 0
},
{
match: [/@(?:exception|throws)/, SPACE, APEX_IDENT_RE],
scope: { 1: 'doctag', 3: 'title.class' },
relevance: 0
},
{ begin: '@[A-Za-z_-]+', scope: 'doctag', relevance: 0 },
{
match: [/(?<=@param)\s+/, APEX_IDENT_RE],
scope: { 2: 'variable' },
relevance: 0
},
{
/* begin: '`',
end: '`',
scope: 'string', */
//excludeBegin: true,
//excludeEnd: true,
contains: [hljs.BACKSLASH_ESCAPE],
relevance: 0,
variants: [
{ begin: '`', end: '`', scope: 'string' }, //, beginScope: 'hidden', endScope: 'hidden' },
{ begin: /'/, end: /'/, scope: 'string' }
],
contains: [
// {match: '`', scope: 'hidden'}
]
}
// TODO : Code Sample in method header
]
});
const COMMENTS = [COMMENT_BLOCK, COMMENT_LINE];
const OPERATORS = {
match: regex.either(...OPERATORS_LIST),
scope: 'operator',
relevance: 0
};
const SALESFORCE_ID = { match: /(?<!\.)\bId\b/, scope: 'type', relevance: 8 };
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*/, /\(/],
beginScope: { 3: 'punctuation' },
end: /\)/,
endScope: 'punctuation',
//scope: 'clause: annotation',
contains: [
{
match: [APEX_IDENT_WORD_RE, /\s*=/],
scope: { 1: 'keyword', 2: 'operator' },
contains: [OPERATORS]
},
STRINGS,
NUMBERS
],
keywords: { literal: LITERALS }
}
];
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
};
let PARAMS_CALL = {
//scope: 'clause: params call',
scope: 'params_call',
begin: /\(/,
beginScope: 'punctuation',
end: /\)/,
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),
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: /\)/,
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: /\{/,
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: /\(/,
end: /\)/,
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,
//scope: 'clause: class_declaration',
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: /\}/,
endScope: 'punctuation',
//scope: 'enum_declaration',
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: [
/(?',
'<!--',
'!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+\*/,
/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\//,
/\Anamespace\b/,
/\bend(\.)?\n/,
'"""',
/\+\+\+/,
/<%/,
'<%#',
'%%>',
'<%%',
':-',
/\bmergesort\(/,
/\bvar\s+env\b/,
/\bdef\b\s\W:/,
/"[^"]+"/, // Quote_string_mode
// /@\w+\[\w+\]/ //moonscript
/\(\*|\*\)/ //mathematica, ocaml
];
return {
name: 'Apex',
aliases: ['apex', 'lightning', 'soql', 'sosl'],
case_insensitive: true, // language is case-insensitive
disableAutodetect: false,
ignoreIllegals: false,
keywords: KEYWORDS,
illegal: ILLEGALS,
contains: [
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
// 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
} */
]
};
/* // * 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
}; */
}