Combinators
Every named building block a tag's grammar is assembled from — a type, a variable, a URI, an email address — is a combinator: a small object that knows how to read exactly one piece of syntax and nothing else. Spec::rule(TypeCombinator::NAME, 'type') in @param's definition, for instance, refers to the combinator below by name.
Given a Cursor positioned at the start of whatever text is still left to parse, a combinator either consumes the part that belongs to it and returns the value it read, or throws NoMatchException and leaves the cursor exactly where it found it. That second case matters: a combinator that partially consumes input before deciding it does not fit would corrupt whichever alternative the surrounding grammar tries next — see Spec::oneOf(), which relies on being able to roll back cleanly.
The Cursor itself offers the handful of reading operations most combinators need, so a combinator rarely has to touch its raw position by hand: peek() and read() for a fixed number of bytes, readWhile()/readUntil() for a run of (or up to) a set of characters, readWord() for the next whitespace-delimited word, readPhpIdentifier()/readPhpQualifiedName() for a name (with or without namespace separators), readLiteral() for an exact piece of text, and readRemainder() for whatever is left entirely.
Built-in Combinators
Fourteen combinators cover every built-in tag's grammar between them:
Name | Returns | Reads |
|---|---|---|
|
| Method or property visibility. |
|
| Everything up to an optional |
|
| |
|
| Everything left, recursively parsed for inline tags. |
|
| An address, up to its closing |
|
| A non-negative integer. |
|
| Letters, digits, |
|
| A single identifier. |
|
| A class, function, method, constant, property or variable. |
|
| A full TypeLang type. |
|
| A well-formed word, per RFC 3986. |
|
| A word that additionally carries a scheme. |
|
| A |
|
| A word beginning with a digit; |
Writing One of Your Own
A combinator that reads one of a fixed set of keywords looks much like the built-in Access combinator above. This one reads a low/medium/high priority level:
Three habits keep a combinator well-behaved inside a larger grammar:
Fail before consuming. Throw
NoMatchExceptionthe moment the input is found not to fit, before reading anything that would belong to what comes next — a combinator that consumes first and validates afterward leaves the cursor somewhere a sibling alternative can no longer make sense of.Consume only your own syntax. Whitespace meant to separate one grammar element from the next is handled by
Spec::sequence(), not by the combinator itself; reading past it leaves nothing for the rest of the grammar to match.Fail rather than guess. When more than one reading of the same input would be plausible, throwing and letting a different alternative in a surrounding
Spec::oneOf()take over is safer than silently picking one.
Once written, a combinator with a NAME constant is used from a tag's $spec exactly like a built-in one — see Grammar. To make it available to the parser, register it on a platform.