src/UriTemplates/Parsers/UriTemplateParser.php
private function parsePunctuation(TokenStream $tokens, AstNode &$currNode, bool $parsingPath): void
    {
        if (($token = $tokens->getCurrent()) === null) {
            return;
        }

        switch ($token->value) {
            case '/':
                if (!$parsingPath) {
                    throw new InvalidArgumentException("Unexpected {$token->type} \"{$token->value}\" in host");
                }

                $currNode->addChild(new AstNode(AstNodeTypes::SEGMENT_DELIMITER, $token->value));
                $tokens->next();
                break;
            case '.':
                // Periods in paths are to be treated as just text
                $nodeType = $parsingPath ? AstNodeTypes::TEXT : AstNodeTypes::SEGMENT_DELIMITER;
                $currNode->addChild(new AstNode($nodeType, $token->value));
                $tokens->next();
                break;
            case '[':
                $parentNodeType = $currNode->type;

                if (
                    $parentNodeType !== AstNodeTypes::HOST
                    && $parentNodeType !== AstNodeTypes::PATH
                    && $parentNodeType !== AstNodeTypes::OPTIONAL_ROUTE_PART
                ) {
                    throw new InvalidArgumentException("Unexpected {$token->type} \"{$token->value}\"");
                }

                $optionalRoutePartNode = new AstNode(AstNodeTypes::OPTIONAL_ROUTE_PART, $token->value);
                $currNode->addChild($optionalRoutePartNode);
                $currNode = $optionalRoutePartNode;
                $tokens->next();

                if ($parsingPath) {
                    $tokens->expect(
                        TokenTypes::T_PUNCTUATION,
                        '/',
                        'Expected optional path part to start with \'/\', got %s'
                    );
                }

                break;
            case ']':
                if ($currNode->type !== AstNodeTypes::OPTIONAL_ROUTE_PART) {
                    // Just treat this as normal text
                    $currNode->addChild(new AstNode(AstNodeTypes::TEXT, $token->value));
                    $tokens->next();
                    break;
                }

                if (!$parsingPath) {
                    /**
                     * Optional parts in hosts must end with '.', eg [foo.[bar.]]example.com
                     * So, make sure that the previous non-optional route part ends with '.'
                     */
                    $isValid = false;

                    for ($i = \count($currNode->children) - 1;$i >= 0;$i--) {
                        $childNode = $currNode->children[$i];

                        if ($childNode->type !== AstNodeTypes::OPTIONAL_ROUTE_PART) {
                            if ($childNode->type === AstNodeTypes::SEGMENT_DELIMITER) {
                                $isValid = true;
                            }

                            break;
                        }
                    }

                    if (!$isValid) {
                        throw new InvalidArgumentException('Expected optional host part to end with \'.\'');
                    }
                }

                // End this optional route part
                $currNode = $currNode->parent;
                $tokens->next();
                break;
            default:
                // Since we handle punctuation inside of variables elsewhere, we'll just treat this as text
                $currNode->addChild(new AstNode(AstNodeTypes::TEXT, $token->value));
                $tokens->next();
                break;
        }
    }
private function parsePunctuation(TokenStream $tokens, AstNode &$currNode, bool $parsingPath): void
    {
        if (($token = $tokens->getCurrent()) === null) {
            return;
        }

        switch ($token->value) {
            case '/':
                if (!$parsingPath) {
                    throw new InvalidArgumentException("Unexpected {$token->type} \"{$token->value}\" in host");
                }

                $currNode->addChild(new AstNode(AstNodeTypes::SEGMENT_DELIMITER, $token->value));
                $tokens->next();
                break;
            case '.':
                // Periods in paths are to be treated as just text
                $nodeType = $parsingPath ? AstNodeTypes::TEXT : AstNodeTypes::SEGMENT_DELIMITER;
                $currNode->addChild(new AstNode($nodeType, $token->value));
                $tokens->next();
                break;
            case '[':
                $parentNodeType = $currNode->type;

                if (
                    $parentNodeType !== AstNodeTypes::HOST
                    && $parentNodeType !== AstNodeTypes::PATH
                    && $parentNodeType !== AstNodeTypes::OPTIONAL_ROUTE_PART
                ) {
                    throw new InvalidArgumentException("Unexpected {$token->type} \"{$token->value}\"");
                }

                $optionalRoutePartNode = new AstNode(AstNodeTypes::OPTIONAL_ROUTE_PART, $token->value);
                $currNode->addChild($optionalRoutePartNode);
                $currNode = $optionalRoutePartNode;
                $tokens->next();

                if ($parsingPath) {
                    $tokens->expect(
                        TokenTypes::T_PUNCTUATION,
                        '/',
                        'Expected optional path part to start with \'/\', got %s'
                    );
                }

                break;
            case ']':
                if ($currNode->type !== AstNodeTypes::OPTIONAL_ROUTE_PART) {
                    // Just treat this as normal text
                    $currNode->addChild(new AstNode(AstNodeTypes::TEXT, $token->value));
                    $tokens->next();
                    break;
                }

                if (!$parsingPath) {
                    /**
                     * Optional parts in hosts must end with '.', eg [foo.[bar.]]example.com
                     * So, make sure that the previous non-optional route part ends with '.'
                     */
                    $isValid = false;

                    for ($i = \count($currNode->children) - 1;$i >= 0;$i--) {
                        $childNode = $currNode->children[$i];

                        if ($childNode->type !== AstNodeTypes::OPTIONAL_ROUTE_PART) {
                            if ($childNode->type === AstNodeTypes::SEGMENT_DELIMITER) {
                                $isValid = true;
                            }

                            break;
                        }
                    }

                    if (!$isValid) {
                        throw new InvalidArgumentException('Expected optional host part to end with \'.\'');
                    }
                }

                // End this optional route part
                $currNode = $currNode->parent;
                $tokens->next();
                break;
            default:
                // Since we handle punctuation inside of variables elsewhere, we'll just treat this as text
                $currNode->addChild(new AstNode(AstNodeTypes::TEXT, $token->value));
                $tokens->next();
                break;
        }
    }
Parses a punctuation token

@param TokenStream $tokens The stream of tokens to parse
@param AstNode $currNode The abstract syntax tree to add nodes to
@param bool $parsingPath Whether or not we're parsing the path (otherwise we're parsing the host)
@throws InvalidArgumentException Thrown if an unexpected punctuation token was found