diff --git a/CHANGES.md b/CHANGES.md index 0e6f4b1bdf..92f6d7b2f5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -15,6 +15,8 @@ These changes should be for the better and should not be super noticeable but if Grammars: +- enh(php) Left and right-side of double colon [Wojciech Kania][] +- enh(php) add PHP constants [Wojciech Kania][] - enh(php) add PHP 8.1 keywords [Wojciech Kania][] - fix(cpp) fix `vector<<` template false positive (#3437) [Josh Goebel][] - enh(php) support First-class Callable Syntax (#3427) [Wojciech Kania][] diff --git a/src/languages/php.js b/src/languages/php.js index 728e744720..7f0be203ae 100644 --- a/src/languages/php.js +++ b/src/languages/php.js @@ -12,12 +12,16 @@ Category: common * */ export default function(hljs) { const regex = hljs.regex; + const IDENT_RE_CORE = '[a-zA-Z0-9_\x7f-\xff]*' + + // negative look-ahead tries to avoid matching patterns that are not + // Perl at all like $ident$, @ident@, etc. + '(?![A-Za-z0-9])(?![$]))'; + const IDENT_RE = regex.concat("([a-zA-Z_\\x7f-\\xff]", IDENT_RE_CORE); + // Will not detect camelCase classes + const PASCAL_CASE_CLASS_NAME_RE = regex.concat("([A-Z]", IDENT_RE_CORE); const VARIABLE = { scope: 'variable', - match: '\\$+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*' + - // negative look-ahead tries to avoid matching patterns that are not - // Perl at all like $ident$, @ident@, etc. - `(?![A-Za-z0-9])(?![$])` + match: '\\$+' + IDENT_RE, }; const PREPROCESSOR = { scope: 'meta', @@ -46,7 +50,7 @@ export default function(hljs) { 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 name + // list of valid whitespaces because non-breaking space might be part of a IDENT_RE const WHITESPACE = '[ \t\n]'; const STRING = { scope: 'string', @@ -313,8 +317,8 @@ export default function(hljs) { regex.concat(WHITESPACE, "+"), // to prevent built ins from being confused as the class constructor call regex.concat("(?!", normalizeKeywords(BUILT_INS).join("\\b|"), "\\b)"), - /\\?\w+/, - regex.concat(WHITESPACE, "*\\("), + regex.concat(/\\?/, IDENT_RE), + regex.concat(WHITESPACE, "*", /\(/), ], scope: { 1: "keyword", @@ -330,7 +334,7 @@ export default function(hljs) { /\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)"), - /\w+/, + IDENT_RE, regex.concat(WHITESPACE, "*"), regex.lookahead(/(?=\()/) ], @@ -339,6 +343,57 @@ export default function(hljs) { } }; + 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)/) + ), + ], + scope: { + 1: "title.class", + }, + }, + { + match: [ + PASCAL_CASE_CLASS_NAME_RE, + /::/, + /class/, + ], + scope: { + 1: "title.class", + 3: "variable.language", + }, + } + ] + }; + return { case_insensitive: false, keywords: KEYWORDS, @@ -351,8 +406,8 @@ export default function(hljs) { { contains: [ { - className: 'doctag', - begin: '@[A-Za-z]+' + scope: 'doctag', + match: '@[A-Za-z]+' } ] } @@ -379,14 +434,18 @@ export default function(hljs) { }, VARIABLE, FUNCTION_INVOKE, + LEFT_AND_RIGHT_SIDE_OF_DOUBLE_COLON, { - // swallow composed identifiers to avoid parsing them as keywords - match: regex.concat( - /(::|->)+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/, - regex.concat("(?!", WHITESPACE, "*\\()"), - /(?![a-zA-Z0-9_\x7f-\xff])/ - ), - // scope:"wrong" + match: [ + /const/, + /\s/, + IDENT_RE, + /\s*=/, + ], + scope: { + 1: "keyword", + 3: "variable.constant", + }, }, CONSTRUCTOR_CALL, { @@ -412,6 +471,7 @@ export default function(hljs) { contains: [ 'self', VARIABLE, + LEFT_AND_RIGHT_SIDE_OF_DOUBLE_COLON, hljs.C_BLOCK_COMMENT_MODE, STRING, NUMBER diff --git a/test/markup/php/functions.expect.txt b/test/markup/php/functions.expect.txt index f6bc43f49a..88c5d94ebe 100644 --- a/test/markup/php/functions.expect.txt +++ b/test/markup/php/functions.expect.txt @@ -13,7 +13,7 @@ $date = new DateTimeImmutable (); $date->format('Y-m-d'); -DateTimeImmutable::createFromMutable(new \DateTime('now')); +DateTimeImmutable::createFromMutable(new \DateTime('now')); str_contains (\strtoupper(substr('abcdef', -2), 'f')); diff --git a/test/markup/php/titles.expect.txt b/test/markup/php/titles.expect.txt new file mode 100644 index 0000000000..05763d78a2 --- /dev/null +++ b/test/markup/php/titles.expect.txt @@ -0,0 +1,26 @@ +final class Example extends Foo { + const FOO='foo'; + + public function __construct( + public readonly string $name = self::FOO + ) {} + + public function getClass(): string { + return DateTimeImmutable::class; + } + + public function getClassFromSelf(): string { + return self::class; + } + + public static function getClassFromStatic(): string { + return static::class; + } + + public static function getParentClass(): string { + return parent::class; + } +} + +$date = DateTimeImmutable::createFromMutable(new \DateTime()); +echo $date::class; diff --git a/test/markup/php/titles.txt b/test/markup/php/titles.txt new file mode 100644 index 0000000000..6ff19f2fc6 --- /dev/null +++ b/test/markup/php/titles.txt @@ -0,0 +1,26 @@ +final class Example extends Foo { + const FOO='foo'; + + public function __construct( + public readonly string $name = self::FOO + ) {} + + public function getClass(): string { + return DateTimeImmutable::class; + } + + public function getClassFromSelf(): string { + return self::class; + } + + public static function getClassFromStatic(): string { + return static::class; + } + + public static function getParentClass(): string { + return parent::class; + } +} + +$date = DateTimeImmutable::createFromMutable(new \DateTime()); +echo $date::class;