hexo/node_modules/highlight.js/lib/languages/php.js

605 lines
14 KiB
JavaScript

/*
Language: PHP
Author: Victor Karamzin <Victor.Karamzin@enterra-inc.com>
Contributors: Evgeny Stepanischev <imbolk@gmail.com>, Ivan Sagalaev <maniac@softwaremaniacs.org>
Website: https://www.php.net
Category: common
*/
/**
* @param {HLJSApi} hljs
* @returns {LanguageDetail}
* */
function php(hljs) {
const regex = hljs.regex;
// negative look-ahead tries to avoid matching patterns that are not
// Perl at all like $ident$, @ident@, etc.
const NOT_PERL_ETC = /(?![A-Za-z0-9])(?![$])/;
const IDENT_RE = regex.concat(
/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/,
NOT_PERL_ETC);
// Will not detect camelCase classes
const PASCAL_CASE_CLASS_NAME_RE = regex.concat(
/(\\?[A-Z][a-z0-9_\x7f-\xff]+|\\?[A-Z]+(?=[A-Z][a-z0-9_\x7f-\xff])){1,}/,
NOT_PERL_ETC);
const VARIABLE = {
scope: 'variable',
match: '\\$+' + IDENT_RE,
};
const PREPROCESSOR = {
scope: 'meta',
variants: [
{ begin: /<\?php/, relevance: 10 }, // boost for obvious PHP
{ begin: /<\?=/ },
// less relevant per PSR-1 which says not to use short-tags
{ begin: /<\?/, relevance: 0.1 },
{ begin: /\?>/ } // end php tag
]
};
const SUBST = {
scope: 'subst',
variants: [
{ begin: /\$\w+/ },
{
begin: /\{\$/,
end: /\}/
}
]
};
const SINGLE_QUOTED = hljs.inherit(hljs.APOS_STRING_MODE, { illegal: null, });
const DOUBLE_QUOTED = hljs.inherit(hljs.QUOTE_STRING_MODE, {
illegal: null,
contains: hljs.QUOTE_STRING_MODE.contains.concat(SUBST),
});
const HEREDOC = hljs.END_SAME_AS_BEGIN({
begin: /<<<[ \t]*(\w+)\n/,
end: /[ \t]*(\w+)\b/,
contains: hljs.QUOTE_STRING_MODE.contains.concat(SUBST),
});
// list of valid whitespaces because non-breaking space might be part of a IDENT_RE
const WHITESPACE = '[ \t\n]';
const STRING = {
scope: 'string',
variants: [
DOUBLE_QUOTED,
SINGLE_QUOTED,
HEREDOC
]
};
const NUMBER = {
scope: 'number',
variants: [
{ begin: `\\b0[bB][01]+(?:_[01]+)*\\b` }, // Binary w/ underscore support
{ begin: `\\b0[oO][0-7]+(?:_[0-7]+)*\\b` }, // Octals w/ underscore support
{ begin: `\\b0[xX][\\da-fA-F]+(?:_[\\da-fA-F]+)*\\b` }, // Hex w/ underscore support
// Decimals w/ underscore support, with optional fragments and scientific exponent (e) suffix.
{ begin: `(?:\\b\\d+(?:_\\d+)*(\\.(?:\\d+(?:_\\d+)*))?|\\B\\.\\d+)(?:[eE][+-]?\\d+)?` }
],
relevance: 0
};
const LITERALS = [
"false",
"null",
"true"
];
const KWS = [
// Magic constants:
// <https://www.php.net/manual/en/language.constants.predefined.php>
"__CLASS__",
"__DIR__",
"__FILE__",
"__FUNCTION__",
"__COMPILER_HALT_OFFSET__",
"__LINE__",
"__METHOD__",
"__NAMESPACE__",
"__TRAIT__",
// Function that look like language construct or language construct that look like function:
// List of keywords that may not require parenthesis
"die",
"echo",
"exit",
"include",
"include_once",
"print",
"require",
"require_once",
// These are not language construct (function) but operate on the currently-executing function and can access the current symbol table
// 'compact extract func_get_arg func_get_args func_num_args get_called_class get_parent_class ' +
// Other keywords:
// <https://www.php.net/manual/en/reserved.php>
// <https://www.php.net/manual/en/language.types.type-juggling.php>
"array",
"abstract",
"and",
"as",
"binary",
"bool",
"boolean",
"break",
"callable",
"case",
"catch",
"class",
"clone",
"const",
"continue",
"declare",
"default",
"do",
"double",
"else",
"elseif",
"empty",
"enddeclare",
"endfor",
"endforeach",
"endif",
"endswitch",
"endwhile",
"enum",
"eval",
"extends",
"final",
"finally",
"float",
"for",
"foreach",
"from",
"global",
"goto",
"if",
"implements",
"instanceof",
"insteadof",
"int",
"integer",
"interface",
"isset",
"iterable",
"list",
"match|0",
"mixed",
"new",
"never",
"object",
"or",
"private",
"protected",
"public",
"readonly",
"real",
"return",
"string",
"switch",
"throw",
"trait",
"try",
"unset",
"use",
"var",
"void",
"while",
"xor",
"yield"
];
const BUILT_INS = [
// Standard PHP library:
// <https://www.php.net/manual/en/book.spl.php>
"Error|0",
"AppendIterator",
"ArgumentCountError",
"ArithmeticError",
"ArrayIterator",
"ArrayObject",
"AssertionError",
"BadFunctionCallException",
"BadMethodCallException",
"CachingIterator",
"CallbackFilterIterator",
"CompileError",
"Countable",
"DirectoryIterator",
"DivisionByZeroError",
"DomainException",
"EmptyIterator",
"ErrorException",
"Exception",
"FilesystemIterator",
"FilterIterator",
"GlobIterator",
"InfiniteIterator",
"InvalidArgumentException",
"IteratorIterator",
"LengthException",
"LimitIterator",
"LogicException",
"MultipleIterator",
"NoRewindIterator",
"OutOfBoundsException",
"OutOfRangeException",
"OuterIterator",
"OverflowException",
"ParentIterator",
"ParseError",
"RangeException",
"RecursiveArrayIterator",
"RecursiveCachingIterator",
"RecursiveCallbackFilterIterator",
"RecursiveDirectoryIterator",
"RecursiveFilterIterator",
"RecursiveIterator",
"RecursiveIteratorIterator",
"RecursiveRegexIterator",
"RecursiveTreeIterator",
"RegexIterator",
"RuntimeException",
"SeekableIterator",
"SplDoublyLinkedList",
"SplFileInfo",
"SplFileObject",
"SplFixedArray",
"SplHeap",
"SplMaxHeap",
"SplMinHeap",
"SplObjectStorage",
"SplObserver",
"SplPriorityQueue",
"SplQueue",
"SplStack",
"SplSubject",
"SplTempFileObject",
"TypeError",
"UnderflowException",
"UnexpectedValueException",
"UnhandledMatchError",
// Reserved interfaces:
// <https://www.php.net/manual/en/reserved.interfaces.php>
"ArrayAccess",
"BackedEnum",
"Closure",
"Fiber",
"Generator",
"Iterator",
"IteratorAggregate",
"Serializable",
"Stringable",
"Throwable",
"Traversable",
"UnitEnum",
"WeakReference",
"WeakMap",
// Reserved classes:
// <https://www.php.net/manual/en/reserved.classes.php>
"Directory",
"__PHP_Incomplete_Class",
"parent",
"php_user_filter",
"self",
"static",
"stdClass"
];
/** Dual-case keywords
*
* ["then","FILE"] =>
* ["then", "THEN", "FILE", "file"]
*
* @param {string[]} items */
const dualCase = (items) => {
/** @type string[] */
const result = [];
items.forEach(item => {
result.push(item);
if (item.toLowerCase() === item) {
result.push(item.toUpperCase());
} else {
result.push(item.toLowerCase());
}
});
return result;
};
const KEYWORDS = {
keyword: KWS,
literal: dualCase(LITERALS),
built_in: BUILT_INS,
};
/**
* @param {string[]} items */
const normalizeKeywords = (items) => {
return items.map(item => {
return item.replace(/\|\d+$/, "");
});
};
const CONSTRUCTOR_CALL = { variants: [
{
match: [
/new/,
regex.concat(WHITESPACE, "+"),
// to prevent built ins from being confused as the class constructor call
regex.concat("(?!", normalizeKeywords(BUILT_INS).join("\\b|"), "\\b)"),
PASCAL_CASE_CLASS_NAME_RE,
],
scope: {
1: "keyword",
4: "title.class",
},
}
] };
const CONSTANT_REFERENCE = regex.concat(IDENT_RE, "\\b(?!\\()");
const LEFT_AND_RIGHT_SIDE_OF_DOUBLE_COLON = { variants: [
{
match: [
regex.concat(
/::/,
regex.lookahead(/(?!class\b)/)
),
CONSTANT_REFERENCE,
],
scope: { 2: "variable.constant", },
},
{
match: [
/::/,
/class/,
],
scope: { 2: "variable.language", },
},
{
match: [
PASCAL_CASE_CLASS_NAME_RE,
regex.concat(
/::/,
regex.lookahead(/(?!class\b)/)
),
CONSTANT_REFERENCE,
],
scope: {
1: "title.class",
3: "variable.constant",
},
},
{
match: [
PASCAL_CASE_CLASS_NAME_RE,
regex.concat(
"::",
regex.lookahead(/(?!class\b)/)
),
],
scope: { 1: "title.class", },
},
{
match: [
PASCAL_CASE_CLASS_NAME_RE,
/::/,
/class/,
],
scope: {
1: "title.class",
3: "variable.language",
},
}
] };
const NAMED_ARGUMENT = {
scope: 'attr',
match: regex.concat(IDENT_RE, regex.lookahead(':'), regex.lookahead(/(?!::)/)),
};
const PARAMS_MODE = {
relevance: 0,
begin: /\(/,
end: /\)/,
keywords: KEYWORDS,
contains: [
NAMED_ARGUMENT,
VARIABLE,
LEFT_AND_RIGHT_SIDE_OF_DOUBLE_COLON,
hljs.C_BLOCK_COMMENT_MODE,
STRING,
NUMBER,
CONSTRUCTOR_CALL,
],
};
const FUNCTION_INVOKE = {
relevance: 0,
match: [
/\b/,
// to prevent keywords from being confused as the function title
regex.concat("(?!fn\\b|function\\b|", normalizeKeywords(KWS).join("\\b|"), "|", normalizeKeywords(BUILT_INS).join("\\b|"), "\\b)"),
IDENT_RE,
regex.concat(WHITESPACE, "*"),
regex.lookahead(/(?=\()/)
],
scope: { 3: "title.function.invoke", },
contains: [ PARAMS_MODE ]
};
PARAMS_MODE.contains.push(FUNCTION_INVOKE);
const ATTRIBUTE_CONTAINS = [
NAMED_ARGUMENT,
LEFT_AND_RIGHT_SIDE_OF_DOUBLE_COLON,
hljs.C_BLOCK_COMMENT_MODE,
STRING,
NUMBER,
CONSTRUCTOR_CALL,
];
const ATTRIBUTES = {
begin: regex.concat(/#\[\s*/, PASCAL_CASE_CLASS_NAME_RE),
beginScope: "meta",
end: /]/,
endScope: "meta",
keywords: {
literal: LITERALS,
keyword: [
'new',
'array',
]
},
contains: [
{
begin: /\[/,
end: /]/,
keywords: {
literal: LITERALS,
keyword: [
'new',
'array',
]
},
contains: [
'self',
...ATTRIBUTE_CONTAINS,
]
},
...ATTRIBUTE_CONTAINS,
{
scope: 'meta',
match: PASCAL_CASE_CLASS_NAME_RE
}
]
};
return {
case_insensitive: false,
keywords: KEYWORDS,
contains: [
ATTRIBUTES,
hljs.HASH_COMMENT_MODE,
hljs.COMMENT('//', '$'),
hljs.COMMENT(
'/\\*',
'\\*/',
{ contains: [
{
scope: 'doctag',
match: '@[A-Za-z]+'
}
] }
),
{
match: /__halt_compiler\(\);/,
keywords: '__halt_compiler',
starts: {
scope: "comment",
end: hljs.MATCH_NOTHING_RE,
contains: [
{
match: /\?>/,
scope: "meta",
endsParent: true
}
]
}
},
PREPROCESSOR,
{
scope: 'variable.language',
match: /\$this\b/
},
VARIABLE,
FUNCTION_INVOKE,
LEFT_AND_RIGHT_SIDE_OF_DOUBLE_COLON,
{
match: [
/const/,
/\s/,
IDENT_RE,
],
scope: {
1: "keyword",
3: "variable.constant",
},
},
CONSTRUCTOR_CALL,
{
scope: 'function',
relevance: 0,
beginKeywords: 'fn function',
end: /[;{]/,
excludeEnd: true,
illegal: '[$%\\[]',
contains: [
{ beginKeywords: 'use', },
hljs.UNDERSCORE_TITLE_MODE,
{
begin: '=>', // No markup, just a relevance booster
endsParent: true
},
{
scope: 'params',
begin: '\\(',
end: '\\)',
excludeBegin: true,
excludeEnd: true,
keywords: KEYWORDS,
contains: [
'self',
VARIABLE,
LEFT_AND_RIGHT_SIDE_OF_DOUBLE_COLON,
hljs.C_BLOCK_COMMENT_MODE,
STRING,
NUMBER
]
},
]
},
{
scope: 'class',
variants: [
{
beginKeywords: "enum",
illegal: /[($"]/
},
{
beginKeywords: "class interface trait",
illegal: /[:($"]/
}
],
relevance: 0,
end: /\{/,
excludeEnd: true,
contains: [
{ beginKeywords: 'extends implements' },
hljs.UNDERSCORE_TITLE_MODE
]
},
// both use and namespace still use "old style" rules (vs multi-match)
// because the namespace name can include `\` and we still want each
// element to be treated as its own *individual* title
{
beginKeywords: 'namespace',
relevance: 0,
end: ';',
illegal: /[.']/,
contains: [ hljs.inherit(hljs.UNDERSCORE_TITLE_MODE, { scope: "title.class" }) ]
},
{
beginKeywords: 'use',
relevance: 0,
end: ';',
contains: [
// TODO: title.function vs title.class
{
match: /\b(as|const|function)\b/,
scope: "keyword"
},
// TODO: could be title.class or title.function
hljs.UNDERSCORE_TITLE_MODE
]
},
STRING,
NUMBER,
]
};
}
module.exports = php;