439 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			439 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
<?php
 | 
						|
/**
 | 
						|
 * This file is part of the Peast package
 | 
						|
 *
 | 
						|
 * (c) Marco Marchiò <marco.mm89@gmail.com>
 | 
						|
 *
 | 
						|
 * For the full copyright and license information refer to the LICENSE file
 | 
						|
 * distributed with this source code
 | 
						|
 */
 | 
						|
namespace Peast\Syntax\JSX;
 | 
						|
 | 
						|
use Peast\Syntax\Token;
 | 
						|
 | 
						|
/**
 | 
						|
 * JSX parser trait
 | 
						|
 * 
 | 
						|
 * @author Marco Marchiò <marco.mm89@gmail.com>
 | 
						|
 */
 | 
						|
trait Parser
 | 
						|
{
 | 
						|
    /**
 | 
						|
     * Creates a JSX node
 | 
						|
     * 
 | 
						|
     * @param string $nodeType Node's type
 | 
						|
     * @param mixed  $position Node's start position
 | 
						|
     * 
 | 
						|
     * @return \Peast\Syntax\Node\Node
 | 
						|
     */
 | 
						|
    protected function createJSXNode($nodeType, $position)
 | 
						|
    {
 | 
						|
        return $this->createNode("JSX\\$nodeType", $position);
 | 
						|
    }
 | 
						|
    
 | 
						|
    /**
 | 
						|
     * Parses a jsx fragment
 | 
						|
     * 
 | 
						|
     * @return \Peast\Syntax\Node\JSX\JSXFragment|null
 | 
						|
     */
 | 
						|
    protected function parseJSXFragment()
 | 
						|
    {
 | 
						|
        $startOpeningToken = $this->scanner->getToken();
 | 
						|
        if (!$startOpeningToken || $startOpeningToken->value !== "<") {
 | 
						|
            return null;
 | 
						|
        }
 | 
						|
        
 | 
						|
        $endOpeningToken = $this->scanner->getNextToken();
 | 
						|
        if (!$endOpeningToken || $endOpeningToken->value !== ">") {
 | 
						|
            return null;
 | 
						|
        }
 | 
						|
        
 | 
						|
        $this->scanner->consumeToken();
 | 
						|
        $this->scanner->consumeToken();
 | 
						|
        
 | 
						|
        $children = $this->parseJSXChildren();
 | 
						|
        
 | 
						|
        if (!($startClosingToken = $this->scanner->consume("<")) ||
 | 
						|
            !$this->scanner->consume("/") ||
 | 
						|
            !$this->scanner->reconsumeCurrentTokenInJSXMode() ||
 | 
						|
            $endOpeningToken->value !== ">") {
 | 
						|
            $this->error();
 | 
						|
        }
 | 
						|
        $this->scanner->consumeToken();
 | 
						|
        
 | 
						|
        //Opening tag
 | 
						|
        $openingNode = $this->createJSXNode(
 | 
						|
            "JSXOpeningFragment",
 | 
						|
            $startOpeningToken
 | 
						|
        );
 | 
						|
        $this->completeNode(
 | 
						|
            $openingNode,
 | 
						|
            $endOpeningToken->location->end
 | 
						|
        );
 | 
						|
        
 | 
						|
        //Closing tag
 | 
						|
        $closingNode = $this->createJSXNode(
 | 
						|
            "JSXClosingFragment",
 | 
						|
            $startClosingToken
 | 
						|
        );
 | 
						|
        $this->completeNode($closingNode);
 | 
						|
        
 | 
						|
        //Fragment
 | 
						|
        $node = $this->createJSXNode("JSXFragment", $startOpeningToken);
 | 
						|
        $node->setOpeningFragment($openingNode);
 | 
						|
        $node->setClosingFragment($closingNode);
 | 
						|
        if ($children) {
 | 
						|
            $node->setChildren($children);
 | 
						|
        }
 | 
						|
        return $this->completeNode($node);
 | 
						|
    }
 | 
						|
    
 | 
						|
    /**
 | 
						|
     * Parses a group of jsx children
 | 
						|
     * 
 | 
						|
     * @return \Peast\Syntax\Node\Node[]|null
 | 
						|
     */
 | 
						|
    protected function parseJSXChildren()
 | 
						|
    {
 | 
						|
        $children = array();
 | 
						|
        while ($child = $this->parseJSXChild()) {
 | 
						|
            $children[] = $child;
 | 
						|
        }
 | 
						|
        return count($children) ? $children : null;
 | 
						|
    }
 | 
						|
    
 | 
						|
    /**
 | 
						|
     * Parses a jsx child
 | 
						|
     * 
 | 
						|
     * @return \Peast\Syntax\Node\Node|null
 | 
						|
     */
 | 
						|
    protected function parseJSXChild()
 | 
						|
    {
 | 
						|
        if ($node = $this->parseJSXText()) {
 | 
						|
            return $node;
 | 
						|
        } elseif ($node = $this->parseJSXFragment()) {
 | 
						|
            return $node;
 | 
						|
        } elseif($node = $this->parseJSXElement()) {
 | 
						|
            return $node;
 | 
						|
        } elseif ($startToken = $this->scanner->consume("{")) {
 | 
						|
            $spread = $this->scanner->consume("...");
 | 
						|
            $exp = $this->parseAssignmentExpression();
 | 
						|
            $midPos = $this->scanner->getPosition();
 | 
						|
            if (($spread && !$exp) || !$this->scanner->consume("}")) {
 | 
						|
                $this->error();
 | 
						|
            }
 | 
						|
            $node = $this->createJSXNode(
 | 
						|
                $spread ? "JSXSpreadChild" : "JSXExpressionContainer",
 | 
						|
                $startToken
 | 
						|
            );
 | 
						|
            if (!$exp) {
 | 
						|
                $exp = $this->createJSXNode("JSXEmptyExpression", $midPos);
 | 
						|
                $this->completeNode($exp, $midPos);
 | 
						|
            }
 | 
						|
            $node->setExpression($exp);
 | 
						|
            return $this->completeNode($node);
 | 
						|
        }
 | 
						|
        return null;
 | 
						|
    }
 | 
						|
    
 | 
						|
    /**
 | 
						|
     * Parses a jsx text
 | 
						|
     * 
 | 
						|
     * @return \Peast\Syntax\Node\JSX\JSXText|null
 | 
						|
     */
 | 
						|
    protected function parseJSXText()
 | 
						|
    {
 | 
						|
        if (!($token = $this->scanner->reconsumeCurrentTokenAsJSXText())) {
 | 
						|
            return null;
 | 
						|
        }
 | 
						|
        $this->scanner->consumeToken();
 | 
						|
        $node = $this->createJSXNode("JSXText", $token);
 | 
						|
        $node->setRaw($token->value);
 | 
						|
        return $this->completeNode($node, $token->location->end);
 | 
						|
    }
 | 
						|
    
 | 
						|
    /**
 | 
						|
     * Parses a jsx element
 | 
						|
     * 
 | 
						|
     * @return \Peast\Syntax\Node\JSX\JSXElement|null
 | 
						|
     */
 | 
						|
    protected function parseJSXElement()
 | 
						|
    {
 | 
						|
        $startOpeningToken = $this->scanner->getToken();
 | 
						|
        if (!$startOpeningToken || $startOpeningToken->value !== "<") {
 | 
						|
            return null;
 | 
						|
        }
 | 
						|
        
 | 
						|
        $nextToken = $this->scanner->getNextToken();
 | 
						|
        if ($nextToken && $nextToken->value === "/") {
 | 
						|
            return null;
 | 
						|
        }
 | 
						|
        
 | 
						|
        $this->scanner->consumeToken();
 | 
						|
        
 | 
						|
        if (!($name = $this->parseJSXIdentifierOrMemberExpression())) {
 | 
						|
            $this->error();
 | 
						|
        }
 | 
						|
        
 | 
						|
        $attributes = $this->parseJSXAttributes();
 | 
						|
        
 | 
						|
        $selfClosing = $this->scanner->consume("/");
 | 
						|
        
 | 
						|
        $endOpeningToken = $this->scanner->reconsumeCurrentTokenInJSXMode();
 | 
						|
        if (!$endOpeningToken || $endOpeningToken->value !== ">") {
 | 
						|
            $this->error();
 | 
						|
        }
 | 
						|
        $this->scanner->consumeToken();
 | 
						|
        
 | 
						|
        if (!$selfClosing) {
 | 
						|
            
 | 
						|
            $children = $this->parseJSXChildren();
 | 
						|
            
 | 
						|
            if (
 | 
						|
                ($startClosingToken = $this->scanner->consume("<")) &&
 | 
						|
                $this->scanner->consume("/") &&
 | 
						|
                ($closingName = $this->parseJSXIdentifierOrMemberExpression()) &&
 | 
						|
                ($endClosingToken = $this->scanner->reconsumeCurrentTokenInJSXMode()) &&
 | 
						|
                ($endClosingToken->value === ">")
 | 
						|
            ) {
 | 
						|
                $this->scanner->consumeToken();
 | 
						|
                if (!$this->isSameJSXElementName($name, $closingName)) {
 | 
						|
                    $this->error("Closing tag does not match opening tag");
 | 
						|
                }
 | 
						|
            } else {
 | 
						|
                $this->error();
 | 
						|
            }
 | 
						|
            
 | 
						|
        }
 | 
						|
        
 | 
						|
        //Opening tag
 | 
						|
        $openingNode = $this->createJSXNode(
 | 
						|
            "JSXOpeningElement",
 | 
						|
            $startOpeningToken
 | 
						|
        );
 | 
						|
        $openingNode->setName($name);
 | 
						|
        $openingNode->setSelfClosing($selfClosing);
 | 
						|
        if ($attributes) {
 | 
						|
            $openingNode->setAttributes($attributes);
 | 
						|
        }
 | 
						|
        $this->completeNode(
 | 
						|
            $openingNode,
 | 
						|
            $endOpeningToken->location->end
 | 
						|
        );
 | 
						|
        
 | 
						|
        //Closing tag
 | 
						|
        $closingNode = null;
 | 
						|
        if (!$selfClosing) {
 | 
						|
            $closingNode = $this->createJSXNode(
 | 
						|
                "JSXClosingElement",
 | 
						|
                $startClosingToken
 | 
						|
            );
 | 
						|
            $closingNode->setName($closingName);
 | 
						|
            $this->completeNode($closingNode);
 | 
						|
        }
 | 
						|
        
 | 
						|
        //Element
 | 
						|
        $node = $this->createJSXNode("JSXElement", $startOpeningToken);
 | 
						|
        $node->setOpeningElement($openingNode);
 | 
						|
        if ($closingNode) {
 | 
						|
            $node->setClosingElement($closingNode);
 | 
						|
            if ($children) {
 | 
						|
                $node->setChildren($children);
 | 
						|
            }
 | 
						|
        }
 | 
						|
        return $this->completeNode($node);
 | 
						|
    }
 | 
						|
    
 | 
						|
    /**
 | 
						|
     * Parses a jsx identifier, namespaced identifier or member expression
 | 
						|
     * 
 | 
						|
     * @param bool $allowMember True to allow member expressions
 | 
						|
     * 
 | 
						|
     * @return \Peast\Syntax\Node\Node|null
 | 
						|
     */
 | 
						|
    protected function parseJSXIdentifierOrMemberExpression($allowMember = true)
 | 
						|
    {
 | 
						|
        $idToken = $this->scanner->reconsumeCurrentTokenInJSXMode();
 | 
						|
        if (!$idToken || $idToken->type !== Token::TYPE_JSX_IDENTIFIER) {
 | 
						|
            return null;
 | 
						|
        }
 | 
						|
        $this->scanner->consumeToken();
 | 
						|
        
 | 
						|
        $idNode = $this->createJSXNode("JSXIdentifier", $idToken);
 | 
						|
        $idNode->setName($idToken->value);
 | 
						|
        $idNode = $this->completeNode($idNode);
 | 
						|
        
 | 
						|
        //Namespaced identifier
 | 
						|
        if ($this->scanner->consume(":")) {
 | 
						|
            
 | 
						|
            $idToken2 = $this->scanner->reconsumeCurrentTokenInJSXMode();
 | 
						|
            if (!$idToken2 || $idToken2->type !== Token::TYPE_JSX_IDENTIFIER) {
 | 
						|
                $this->error();
 | 
						|
            }
 | 
						|
            $this->scanner->consumeToken();
 | 
						|
            
 | 
						|
            $idNode2 = $this->createJSXNode("JSXIdentifier", $idToken2);
 | 
						|
            $idNode2->setName($idToken2->value);
 | 
						|
            $idNode2 = $this->completeNode($idNode2);
 | 
						|
            
 | 
						|
            $node = $this->createJSXNode("JSXNamespacedName", $idToken);
 | 
						|
            $node->setNamespace($idNode);
 | 
						|
            $node->setName($idNode2);
 | 
						|
            return $this->completeNode($node);
 | 
						|
            
 | 
						|
        }
 | 
						|
        
 | 
						|
        //Get following identifiers
 | 
						|
        $nextIds = array();
 | 
						|
        if ($allowMember) {
 | 
						|
            while ($this->scanner->consume(".")) {
 | 
						|
                $nextId = $this->scanner->reconsumeCurrentTokenInJSXMode();
 | 
						|
                if (!$nextId || $nextId->type !== Token::TYPE_JSX_IDENTIFIER) {
 | 
						|
                    $this->error();
 | 
						|
                }
 | 
						|
                $this->scanner->consumeToken();
 | 
						|
                $nextIds[] = $nextId;
 | 
						|
            }
 | 
						|
        }
 | 
						|
        
 | 
						|
        //Create the member expression if required
 | 
						|
        $objectNode = $idNode;
 | 
						|
        foreach ($nextIds as $nid) {
 | 
						|
            $propEnd = $nid->location->end;
 | 
						|
            $propNode = $this->createJSXNode("JSXIdentifier", $nid);
 | 
						|
            $propNode->setName($nid->value);
 | 
						|
            $propNode = $this->completeNode($propNode, $propEnd);
 | 
						|
            
 | 
						|
            $node = $this->createJSXNode("JSXMemberExpression", $objectNode);
 | 
						|
            $node->setObject($objectNode);
 | 
						|
            $node->setProperty($propNode);
 | 
						|
            $objectNode = $this->completeNode($node, $propEnd);
 | 
						|
        }
 | 
						|
        
 | 
						|
        return $objectNode;
 | 
						|
    }
 | 
						|
    
 | 
						|
    /**
 | 
						|
     * Parses a jsx attributes list
 | 
						|
     * 
 | 
						|
     * @return \Peast\Syntax\Node\Node[]|null
 | 
						|
     */
 | 
						|
    protected function parseJSXAttributes()
 | 
						|
    {
 | 
						|
        $attributes = array();
 | 
						|
        while (
 | 
						|
            ($attr = $this->parseJSXSpreadAttribute()) ||
 | 
						|
            ($attr = $this->parseJSXAttribute())
 | 
						|
        ) {
 | 
						|
            $attributes[] = $attr;
 | 
						|
        }
 | 
						|
        return count($attributes) ? $attributes : null;
 | 
						|
    }
 | 
						|
    
 | 
						|
    /**
 | 
						|
     * Parses a jsx spread attribute
 | 
						|
     * 
 | 
						|
     * @return \Peast\Syntax\Node\JSX\JSXSpreadAttribute|null
 | 
						|
     */
 | 
						|
    protected function parseJSXSpreadAttribute()
 | 
						|
    {
 | 
						|
        if (!($openToken = $this->scanner->consume("{"))) {
 | 
						|
            return null;
 | 
						|
        }
 | 
						|
        
 | 
						|
        if (
 | 
						|
            $this->scanner->consume("...") &&
 | 
						|
            ($exp = $this->parseAssignmentExpression()) &&
 | 
						|
            $this->scanner->consume("}")
 | 
						|
        ) {
 | 
						|
            $node = $this->createJSXNode("JSXSpreadAttribute", $openToken);
 | 
						|
            $node->setArgument($exp);
 | 
						|
            return $this->completeNode($node);
 | 
						|
        }
 | 
						|
        
 | 
						|
        $this->error();
 | 
						|
    }
 | 
						|
    
 | 
						|
    /**
 | 
						|
     * Parses a jsx spread attribute
 | 
						|
     * 
 | 
						|
     * @return \Peast\Syntax\Node\JSX\JSXSpreadAttribute|null
 | 
						|
     */
 | 
						|
    protected function parseJSXAttribute()
 | 
						|
    {
 | 
						|
        if (!($name = $this->parseJSXIdentifierOrMemberExpression(false))) {
 | 
						|
            return null;
 | 
						|
        }
 | 
						|
        
 | 
						|
        $value = null;
 | 
						|
        if ($this->scanner->consume("=")) {
 | 
						|
            $strToken = $this->scanner->reconsumeCurrentTokenInJSXMode();
 | 
						|
            if ($strToken && $strToken->type === Token::TYPE_STRING_LITERAL) {
 | 
						|
                $this->scanner->consumeToken();
 | 
						|
                $value = $this->createNode("StringLiteral", $strToken);
 | 
						|
                $value->setRaw($strToken->value);
 | 
						|
                $value = $this->completeNode($value);
 | 
						|
            } elseif ($startExp = $this->scanner->consume("{")) {
 | 
						|
                
 | 
						|
                if (
 | 
						|
                    ($exp = $this->parseAssignmentExpression()) &&
 | 
						|
                    $this->scanner->consume("}")
 | 
						|
                ) {
 | 
						|
                    
 | 
						|
                    $value = $this->createJSXNode(
 | 
						|
                        "JSXExpressionContainer",
 | 
						|
                        $startExp
 | 
						|
                    );
 | 
						|
                    $value->setExpression($exp);
 | 
						|
                    $value = $this->completeNode($value);
 | 
						|
                    
 | 
						|
                } else {
 | 
						|
                    $this->error();
 | 
						|
                }
 | 
						|
                
 | 
						|
            } elseif (
 | 
						|
                !($value = $this->parseJSXFragment()) &&
 | 
						|
                !($value = $this->parseJSXElement())
 | 
						|
            ) {
 | 
						|
                $this->error();
 | 
						|
            }
 | 
						|
        }
 | 
						|
        
 | 
						|
        $node = $this->createJSXNode("JSXAttribute", $name);
 | 
						|
        $node->setName($name);
 | 
						|
        if ($value) {
 | 
						|
            $node->setValue($value);
 | 
						|
        }
 | 
						|
        return $this->completeNode($node);
 | 
						|
    }
 | 
						|
    
 | 
						|
    /**
 | 
						|
     * Checks that 2 tag names are equal
 | 
						|
     * 
 | 
						|
     * @param \Peast\Syntax\Node\Node   $n1 First name
 | 
						|
     * @param \Peast\Syntax\Node\Node   $n2 Second name
 | 
						|
     * 
 | 
						|
     * @return bool
 | 
						|
     */
 | 
						|
    protected function isSameJSXElementName($n1, $n2)
 | 
						|
    {
 | 
						|
        $type = $n1->getType();
 | 
						|
        if ($type !== $n2->getType()) {
 | 
						|
            return false;
 | 
						|
        } elseif ($type === "JSXNamespacedName") {
 | 
						|
            return $this->isSameJSXElementName(
 | 
						|
                $n1->getNamespace(), $n2->getNamespace()
 | 
						|
            ) && $this->isSameJSXElementName(
 | 
						|
                $n1->getName(), $n2->getName()
 | 
						|
            );
 | 
						|
        } elseif ($type === "JSXMemberExpression") {
 | 
						|
            return $this->isSameJSXElementName(
 | 
						|
                $n1->getObject(), $n2->getObject()
 | 
						|
            ) && $this->isSameJSXElementName(
 | 
						|
                $n1->getProperty(), $n2->getProperty()
 | 
						|
            );
 | 
						|
        }
 | 
						|
        return $type === "JSXIdentifier" && $n1->getName() === $n2->getName();
 | 
						|
    }
 | 
						|
}
 |