URL
Every part of an address, in the order RFC 3986 writes them:
https://user:pass@example.com:8080/over/there?name=ferret&sort=asc#nose
Almost everything here is optional, and the root rule says exactly that: a
scheme and a host are required, the credentials, the port, the path, the query
and the fragment each carry a ?. Reading http://example.com and the line
above takes the same seven rules.
The lexer keeps two ambiguities away from the parser. A percent escape belongs
to the token that contains it, so %20 is part of a name rather than
punctuation of its own. And a number is only read as one where a name does not
carry on past it - T_DIGITS ends with (?![a-zA-Z0-9.+\-%]) - so the 1a
of a host is a name, not a number followed by a letter.
Grammar
1/** 2 * ----------------------------------------------------------------------------- 3 * URL 4 * ----------------------------------------------------------------------------- 5 * 6 * The scheme, the credentials, the host, the port, the path, the query and the 7 * fragment an address is written of. 8 * 9 * @see https://www.rfc-editor.org/rfc/rfc3986 10 */ 11 12%pragma root Url 13 14%skip T_WHITESPACE \s++ 15 16%token T_SCHEME_SEPARATOR :// 17 18/** 19 * A number is only read as one where a name does not go on past it, so the 20 * "1a" of a host is a name rather than a number followed by a name. 21 */ 22%token T_DIGITS [0-9]++(?![a-zA-Z0-9.+\-%]) 23 24// A character written as itself, or as the "%" of its code 25%token T_STRING (?:[a-zA-Z~0-9]|%[0-9a-fA-F]{2})(?:[a-zA-Z0-9.+\-_]|%[0-9a-fA-F]{2})*+ 26 27%token T_DOUBLE_COLON :: 28%token T_COLON : 29%token T_SLASH / 30%token T_AT @ 31%token T_QUESTION_MARK \? 32%token T_AMPERSAND & 33%token T_EQUAL = 34%token T_HASH # 35%token T_BRACKET_OPEN \[ 36%token T_BRACKET_CLOSE \] 37 38Url 39 : Scheme() ::T_SCHEME_SEPARATOR:: 40 Login()? 41 Host() 42 (::T_COLON:: Port())? 43 (::T_SLASH:: Path()?)? 44 Query()? 45 Fragment()? 46 ; 47 48Scheme 49 : <T_STRING> 50 ; 51 52// user:password@ 53Login 54 : User() (::T_COLON:: Password())? ::T_AT:: 55 ; 56 57User 58 : Text() 59 ; 60 61Password 62 : Text() 63 ; 64 65Host 66 : ::T_SLASH::? HostName() 67 ; 68 69HostName 70 : ::T_BRACKET_OPEN:: IPv6Host() ::T_BRACKET_CLOSE:: 71 | Text() 72 ; 73 74IPv6Host 75 : ::T_DOUBLE_COLON::? Text() ((::T_COLON:: | ::T_DOUBLE_COLON::) Text())* 76 ; 77 78Port 79 : <T_DIGITS> 80 ; 81 82Path 83 : Text() (::T_SLASH:: Text())* ::T_SLASH::? 84 ; 85 86Query 87 : ::T_QUESTION_MARK:: Search() 88 ; 89 90Search 91 : SearchParameter() (::T_AMPERSAND:: SearchParameter())* 92 ; 93 94SearchParameter 95 : Text() (::T_EQUAL:: Text())? 96 ; 97 98Fragment 99 : ::T_HASH:: Text() 100 ; 101 102Text 103 : <T_STRING> 104 | <T_DIGITS> 105 ;
Usage
1use Phplrt\Compiler\Compiler; 2use Phplrt\Source\File; 3use Phplrt\Source\Source; 4 5$parser = new Compiler() 6 ->load(new File(__DIR__ . '/grammar.pp3')) 7 ->getParser(); 8 9$url = $parser->parse(new Source('https://example.com:8080/a/b?c=d#e'));
parse_url() is faster and shorter, and this grammar is still worth reading:
it says out loud what that function decides quietly, and it tells you where
an address goes wrong instead of returning false.
25+ more grammars. phplrt/grammars collects ready to read grammars for real languages - JSON5, TSV, semantic versions, DQL, PHQL, JMS types, PSR-5 and Doctrine annotations, Symfony expressions, Go! AOP pointcuts, Praspel contracts and more - each with sample inputs and a test that keeps it honest.