phplrt 4.0

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.