/* Language: PHP Author: Victor Karamzin Contributors: Evgeny Stepanischev , Ivan Sagalaev 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: // "__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: // // "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: // "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: // "ArrayAccess", "BackedEnum", "Closure", "Fiber", "Generator", "Iterator", "IteratorAggregate", "Serializable", "Stringable", "Throwable", "Traversable", "UnitEnum", "WeakReference", "WeakMap", // Reserved classes: // "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;