585 lines
20 KiB
Python
585 lines
20 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright JS Foundation and other contributors, https://js.foundation/
|
|
#
|
|
# Redistribution and use in source and binary forms, with or without
|
|
# modification, are permitted provided that the following conditions are met:
|
|
#
|
|
# * Redistributions of source code must retain the above copyright
|
|
# notice, this list of conditions and the following disclaimer.
|
|
# * Redistributions in binary form must reproduce the above copyright
|
|
# notice, this list of conditions and the following disclaimer in the
|
|
# documentation and/or other materials provided with the distribution.
|
|
#
|
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
# ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
|
|
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
|
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
from __future__ import absolute_import, unicode_literals
|
|
|
|
from .compat import uchr
|
|
from .character import Character
|
|
from . import jsx_nodes as JSXNode
|
|
from .jsx_syntax import JSXSyntax
|
|
from . import nodes as Node
|
|
from .parser import Marker, Parser
|
|
from .token import Token, TokenName
|
|
from .xhtml_entities import XHTMLEntities
|
|
|
|
|
|
class MetaJSXElement(object):
|
|
def __init__(self, node=None, opening=None, closing=None, children=None):
|
|
self.node = node
|
|
self.opening = opening
|
|
self.closing = closing
|
|
self.children = children
|
|
|
|
|
|
class JSXToken(object):
|
|
Identifier = 100
|
|
Text = 101
|
|
|
|
|
|
class RawJSXToken(object):
|
|
def __init__(self, type=None, value=None, lineNumber=None, lineStart=None, start=None, end=None):
|
|
self.type = type
|
|
self.value = value
|
|
self.lineNumber = lineNumber
|
|
self.lineStart = lineStart
|
|
self.start = start
|
|
self.end = end
|
|
|
|
|
|
TokenName[JSXToken.Identifier] = "JSXIdentifier"
|
|
TokenName[JSXToken.Text] = "JSXText"
|
|
|
|
|
|
# Fully qualified element name, e.g. <svg:path> returns "svg:path"
|
|
def getQualifiedElementName(elementName):
|
|
typ = elementName.type
|
|
if typ is JSXSyntax.JSXIdentifier:
|
|
id = elementName
|
|
qualifiedName = id.name
|
|
elif typ is JSXSyntax.JSXNamespacedName:
|
|
ns = elementName
|
|
qualifiedName = getQualifiedElementName(ns.namespace) + ':' + getQualifiedElementName(ns.name)
|
|
elif typ is JSXSyntax.JSXMemberExpression:
|
|
expr = elementName
|
|
qualifiedName = getQualifiedElementName(expr.object) + '.' + getQualifiedElementName(expr.property)
|
|
|
|
return qualifiedName
|
|
|
|
|
|
class JSXParser(Parser):
|
|
def __init__(self, code, options, delegate):
|
|
super(JSXParser, self).__init__(code, options, delegate)
|
|
|
|
def parsePrimaryExpression(self):
|
|
return self.parseJSXRoot() if self.match('<') else super(JSXParser, self).parsePrimaryExpression()
|
|
|
|
def startJSX(self):
|
|
# Unwind the scanner before the lookahead token.
|
|
self.scanner.index = self.startMarker.index
|
|
self.scanner.lineNumber = self.startMarker.line
|
|
self.scanner.lineStart = self.startMarker.index - self.startMarker.column
|
|
|
|
def finishJSX(self):
|
|
# Prime the next lookahead.
|
|
self.nextToken()
|
|
|
|
def reenterJSX(self):
|
|
self.startJSX()
|
|
self.expectJSX('}')
|
|
|
|
# Pop the closing '}' added from the lookahead.
|
|
if self.config.tokens:
|
|
self.tokens.pop()
|
|
|
|
def createJSXNode(self):
|
|
self.collectComments()
|
|
return Marker(
|
|
index=self.scanner.index,
|
|
line=self.scanner.lineNumber,
|
|
column=self.scanner.index - self.scanner.lineStart
|
|
)
|
|
|
|
def createJSXChildNode(self):
|
|
return Marker(
|
|
index=self.scanner.index,
|
|
line=self.scanner.lineNumber,
|
|
column=self.scanner.index - self.scanner.lineStart
|
|
)
|
|
|
|
def scanXHTMLEntity(self, quote):
|
|
result = '&'
|
|
|
|
valid = True
|
|
terminated = False
|
|
numeric = False
|
|
hex = False
|
|
|
|
while not self.scanner.eof() and valid and not terminated:
|
|
ch = self.scanner.source[self.scanner.index]
|
|
if ch == quote:
|
|
break
|
|
|
|
terminated = (ch == ';')
|
|
result += ch
|
|
self.scanner.index += 1
|
|
if not terminated:
|
|
length = len(result)
|
|
if length == 2:
|
|
# e.g. '{'
|
|
numeric = (ch == '#')
|
|
elif length == 3:
|
|
if numeric:
|
|
# e.g. 'A'
|
|
hex = ch == 'x'
|
|
valid = hex or Character.isDecimalDigit(ch)
|
|
numeric = numeric and not hex
|
|
else:
|
|
valid = valid and not (numeric and not Character.isDecimalDigit(ch))
|
|
valid = valid and not (hex and not Character.isHexDigit(ch))
|
|
|
|
if valid and terminated and len(result) > 2:
|
|
# e.g. 'A' becomes just '#x41'
|
|
st = result[1:-1]
|
|
if numeric and len(st) > 1:
|
|
result = uchr(int(st[1:], 10))
|
|
elif hex and len(st) > 2:
|
|
result = uchr(int(st[2:], 16))
|
|
elif not numeric and not hex and st in XHTMLEntities:
|
|
result = XHTMLEntities[st]
|
|
|
|
return result
|
|
|
|
# Scan the next JSX token. This replaces Scanner#lex when in JSX mode.
|
|
|
|
def lexJSX(self):
|
|
ch = self.scanner.source[self.scanner.index]
|
|
|
|
# < > / : = { }
|
|
if ch in ('<', '>', '/', ':', '=', '{', '}'):
|
|
value = self.scanner.source[self.scanner.index]
|
|
self.scanner.index += 1
|
|
return RawJSXToken(
|
|
type=Token.Punctuator,
|
|
value=value,
|
|
lineNumber=self.scanner.lineNumber,
|
|
lineStart=self.scanner.lineStart,
|
|
start=self.scanner.index - 1,
|
|
end=self.scanner.index
|
|
)
|
|
|
|
# " '
|
|
if ch in ('\'', '"'):
|
|
start = self.scanner.index
|
|
quote = self.scanner.source[self.scanner.index]
|
|
self.scanner.index += 1
|
|
str = ''
|
|
while not self.scanner.eof():
|
|
ch = self.scanner.source[self.scanner.index]
|
|
self.scanner.index += 1
|
|
if ch == quote:
|
|
break
|
|
elif ch == '&':
|
|
str += self.scanXHTMLEntity(quote)
|
|
else:
|
|
str += ch
|
|
|
|
return RawJSXToken(
|
|
type=Token.StringLiteral,
|
|
value=str,
|
|
lineNumber=self.scanner.lineNumber,
|
|
lineStart=self.scanner.lineStart,
|
|
start=start,
|
|
end=self.scanner.index
|
|
)
|
|
|
|
# ... or .
|
|
if ch == '.':
|
|
start = self.scanner.index
|
|
if self.scanner.source[start + 1:start + 3] == '..':
|
|
value = '...'
|
|
self.scanner.index += 3
|
|
else:
|
|
value = '.'
|
|
self.scanner.index += 1
|
|
return RawJSXToken(
|
|
type=Token.Punctuator,
|
|
value=value,
|
|
lineNumber=self.scanner.lineNumber,
|
|
lineStart=self.scanner.lineStart,
|
|
start=start,
|
|
end=self.scanner.index
|
|
)
|
|
|
|
# `
|
|
if ch == '`':
|
|
# Only placeholder, since it will be rescanned as a real assignment expression.
|
|
return RawJSXToken(
|
|
type=Token.Template,
|
|
value='',
|
|
lineNumber=self.scanner.lineNumber,
|
|
lineStart=self.scanner.lineStart,
|
|
start=self.scanner.index,
|
|
end=self.scanner.index
|
|
)
|
|
|
|
# Identifier can not contain backslash (char code 92).
|
|
if Character.isIdentifierStart(ch) and ch != '\\':
|
|
start = self.scanner.index
|
|
self.scanner.index += 1
|
|
while not self.scanner.eof():
|
|
ch = self.scanner.source[self.scanner.index]
|
|
if Character.isIdentifierPart(ch) and ch != '\\':
|
|
self.scanner.index += 1
|
|
elif ch == '-':
|
|
# Hyphen (char code 45) can be part of an identifier.
|
|
self.scanner.index += 1
|
|
else:
|
|
break
|
|
|
|
id = self.scanner.source[start:self.scanner.index]
|
|
return RawJSXToken(
|
|
type=JSXToken.Identifier,
|
|
value=id,
|
|
lineNumber=self.scanner.lineNumber,
|
|
lineStart=self.scanner.lineStart,
|
|
start=start,
|
|
end=self.scanner.index
|
|
)
|
|
|
|
return self.scanner.lex()
|
|
|
|
def nextJSXToken(self):
|
|
self.collectComments()
|
|
|
|
self.startMarker.index = self.scanner.index
|
|
self.startMarker.line = self.scanner.lineNumber
|
|
self.startMarker.column = self.scanner.index - self.scanner.lineStart
|
|
token = self.lexJSX()
|
|
self.lastMarker.index = self.scanner.index
|
|
self.lastMarker.line = self.scanner.lineNumber
|
|
self.lastMarker.column = self.scanner.index - self.scanner.lineStart
|
|
|
|
if self.config.tokens:
|
|
self.tokens.append(self.convertToken(token))
|
|
|
|
return token
|
|
|
|
def nextJSXText(self):
|
|
self.startMarker.index = self.scanner.index
|
|
self.startMarker.line = self.scanner.lineNumber
|
|
self.startMarker.column = self.scanner.index - self.scanner.lineStart
|
|
|
|
start = self.scanner.index
|
|
|
|
text = ''
|
|
while not self.scanner.eof():
|
|
ch = self.scanner.source[self.scanner.index]
|
|
if ch in ('{', '<'):
|
|
break
|
|
|
|
self.scanner.index += 1
|
|
text += ch
|
|
if Character.isLineTerminator(ch):
|
|
self.scanner.lineNumber += 1
|
|
if ch == '\r' and self.scanner.source[self.scanner.index] == '\n':
|
|
self.scanner.index += 1
|
|
|
|
self.scanner.lineStart = self.scanner.index
|
|
|
|
self.lastMarker.index = self.scanner.index
|
|
self.lastMarker.line = self.scanner.lineNumber
|
|
self.lastMarker.column = self.scanner.index - self.scanner.lineStart
|
|
|
|
token = RawJSXToken(
|
|
type=JSXToken.Text,
|
|
value=text,
|
|
lineNumber=self.scanner.lineNumber,
|
|
lineStart=self.scanner.lineStart,
|
|
start=start,
|
|
end=self.scanner.index
|
|
)
|
|
|
|
if text and self.config.tokens:
|
|
self.tokens.append(self.convertToken(token))
|
|
|
|
return token
|
|
|
|
def peekJSXToken(self):
|
|
state = self.scanner.saveState()
|
|
self.scanner.scanComments()
|
|
next = self.lexJSX()
|
|
self.scanner.restoreState(state)
|
|
|
|
return next
|
|
|
|
# Expect the next JSX token to match the specified punctuator.
|
|
# If not, an exception will be thrown.
|
|
|
|
def expectJSX(self, value):
|
|
token = self.nextJSXToken()
|
|
if token.type is not Token.Punctuator or token.value != value:
|
|
self.throwUnexpectedToken(token)
|
|
|
|
# Return True if the next JSX token matches the specified punctuator.
|
|
|
|
def matchJSX(self, *value):
|
|
next = self.peekJSXToken()
|
|
return next.type is Token.Punctuator and next.value in value
|
|
|
|
def parseJSXIdentifier(self):
|
|
node = self.createJSXNode()
|
|
token = self.nextJSXToken()
|
|
if token.type is not JSXToken.Identifier:
|
|
self.throwUnexpectedToken(token)
|
|
|
|
return self.finalize(node, JSXNode.JSXIdentifier(token.value))
|
|
|
|
def parseJSXElementName(self):
|
|
node = self.createJSXNode()
|
|
elementName = self.parseJSXIdentifier()
|
|
|
|
if self.matchJSX(':'):
|
|
namespace = elementName
|
|
self.expectJSX(':')
|
|
name = self.parseJSXIdentifier()
|
|
elementName = self.finalize(node, JSXNode.JSXNamespacedName(namespace, name))
|
|
elif self.matchJSX('.'):
|
|
while self.matchJSX('.'):
|
|
object = elementName
|
|
self.expectJSX('.')
|
|
property = self.parseJSXIdentifier()
|
|
elementName = self.finalize(node, JSXNode.JSXMemberExpression(object, property))
|
|
|
|
return elementName
|
|
|
|
def parseJSXAttributeName(self):
|
|
node = self.createJSXNode()
|
|
|
|
identifier = self.parseJSXIdentifier()
|
|
if self.matchJSX(':'):
|
|
namespace = identifier
|
|
self.expectJSX(':')
|
|
name = self.parseJSXIdentifier()
|
|
attributeName = self.finalize(node, JSXNode.JSXNamespacedName(namespace, name))
|
|
else:
|
|
attributeName = identifier
|
|
|
|
return attributeName
|
|
|
|
def parseJSXStringLiteralAttribute(self):
|
|
node = self.createJSXNode()
|
|
token = self.nextJSXToken()
|
|
if token.type is not Token.StringLiteral:
|
|
self.throwUnexpectedToken(token)
|
|
|
|
raw = self.getTokenRaw(token)
|
|
return self.finalize(node, Node.Literal(token.value, raw))
|
|
|
|
def parseJSXExpressionAttribute(self):
|
|
node = self.createJSXNode()
|
|
|
|
self.expectJSX('{')
|
|
self.finishJSX()
|
|
|
|
if self.match('}'):
|
|
self.tolerateError('JSX attributes must only be assigned a non-empty expression')
|
|
|
|
expression = self.parseAssignmentExpression()
|
|
self.reenterJSX()
|
|
|
|
return self.finalize(node, JSXNode.JSXExpressionContainer(expression))
|
|
|
|
def parseJSXAttributeValue(self):
|
|
if self.matchJSX('{'):
|
|
return self.parseJSXExpressionAttribute()
|
|
if self.matchJSX('<'):
|
|
return self.parseJSXElement()
|
|
|
|
return self.parseJSXStringLiteralAttribute()
|
|
|
|
def parseJSXNameValueAttribute(self):
|
|
node = self.createJSXNode()
|
|
name = self.parseJSXAttributeName()
|
|
value = None
|
|
if self.matchJSX('='):
|
|
self.expectJSX('=')
|
|
value = self.parseJSXAttributeValue()
|
|
|
|
return self.finalize(node, JSXNode.JSXAttribute(name, value))
|
|
|
|
def parseJSXSpreadAttribute(self):
|
|
node = self.createJSXNode()
|
|
self.expectJSX('{')
|
|
self.expectJSX('...')
|
|
|
|
self.finishJSX()
|
|
argument = self.parseAssignmentExpression()
|
|
self.reenterJSX()
|
|
|
|
return self.finalize(node, JSXNode.JSXSpreadAttribute(argument))
|
|
|
|
def parseJSXAttributes(self):
|
|
attributes = []
|
|
|
|
while not self.matchJSX('/', '>'):
|
|
attribute = self.parseJSXSpreadAttribute() if self.matchJSX('{') else self.parseJSXNameValueAttribute()
|
|
attributes.append(attribute)
|
|
|
|
return attributes
|
|
|
|
def parseJSXOpeningElement(self):
|
|
node = self.createJSXNode()
|
|
|
|
self.expectJSX('<')
|
|
name = self.parseJSXElementName()
|
|
attributes = self.parseJSXAttributes()
|
|
selfClosing = self.matchJSX('/')
|
|
if selfClosing:
|
|
self.expectJSX('/')
|
|
|
|
self.expectJSX('>')
|
|
|
|
return self.finalize(node, JSXNode.JSXOpeningElement(name, selfClosing, attributes))
|
|
|
|
def parseJSXBoundaryElement(self):
|
|
node = self.createJSXNode()
|
|
|
|
self.expectJSX('<')
|
|
if self.matchJSX('/'):
|
|
self.expectJSX('/')
|
|
elementName = self.parseJSXElementName()
|
|
self.expectJSX('>')
|
|
return self.finalize(node, JSXNode.JSXClosingElement(elementName))
|
|
|
|
name = self.parseJSXElementName()
|
|
attributes = self.parseJSXAttributes()
|
|
selfClosing = self.matchJSX('/')
|
|
if selfClosing:
|
|
self.expectJSX('/')
|
|
|
|
self.expectJSX('>')
|
|
|
|
return self.finalize(node, JSXNode.JSXOpeningElement(name, selfClosing, attributes))
|
|
|
|
def parseJSXEmptyExpression(self):
|
|
node = self.createJSXChildNode()
|
|
self.collectComments()
|
|
self.lastMarker.index = self.scanner.index
|
|
self.lastMarker.line = self.scanner.lineNumber
|
|
self.lastMarker.column = self.scanner.index - self.scanner.lineStart
|
|
return self.finalize(node, JSXNode.JSXEmptyExpression())
|
|
|
|
def parseJSXExpressionContainer(self):
|
|
node = self.createJSXNode()
|
|
self.expectJSX('{')
|
|
|
|
if self.matchJSX('}'):
|
|
expression = self.parseJSXEmptyExpression()
|
|
self.expectJSX('}')
|
|
else:
|
|
self.finishJSX()
|
|
expression = self.parseAssignmentExpression()
|
|
self.reenterJSX()
|
|
|
|
return self.finalize(node, JSXNode.JSXExpressionContainer(expression))
|
|
|
|
def parseJSXChildren(self):
|
|
children = []
|
|
|
|
while not self.scanner.eof():
|
|
node = self.createJSXChildNode()
|
|
token = self.nextJSXText()
|
|
if token.start < token.end:
|
|
raw = self.getTokenRaw(token)
|
|
child = self.finalize(node, JSXNode.JSXText(token.value, raw))
|
|
children.append(child)
|
|
|
|
if self.scanner.source[self.scanner.index] == '{':
|
|
container = self.parseJSXExpressionContainer()
|
|
children.append(container)
|
|
else:
|
|
break
|
|
|
|
return children
|
|
|
|
def parseComplexJSXElement(self, el):
|
|
stack = []
|
|
|
|
while not self.scanner.eof():
|
|
el.children.extend(self.parseJSXChildren())
|
|
node = self.createJSXChildNode()
|
|
element = self.parseJSXBoundaryElement()
|
|
if element.type is JSXSyntax.JSXOpeningElement:
|
|
opening = element
|
|
if opening.selfClosing:
|
|
child = self.finalize(node, JSXNode.JSXElement(opening, [], None))
|
|
el.children.append(child)
|
|
else:
|
|
stack.append(el)
|
|
el = MetaJSXElement(
|
|
node=node,
|
|
opening=opening,
|
|
closing=None,
|
|
children=[],
|
|
)
|
|
|
|
if element.type is JSXSyntax.JSXClosingElement:
|
|
el.closing = element
|
|
open = getQualifiedElementName(el.opening.name)
|
|
close = getQualifiedElementName(el.closing.name)
|
|
if open != close:
|
|
self.tolerateError('Expected corresponding JSX closing tag for %0', open)
|
|
|
|
if stack:
|
|
child = self.finalize(el.node, JSXNode.JSXElement(el.opening, el.children, el.closing))
|
|
el = stack[-1]
|
|
el.children.append(child)
|
|
stack.pop()
|
|
else:
|
|
break
|
|
|
|
return el
|
|
|
|
def parseJSXElement(self):
|
|
node = self.createJSXNode()
|
|
|
|
opening = self.parseJSXOpeningElement()
|
|
children = []
|
|
closing = None
|
|
|
|
if not opening.selfClosing:
|
|
el = self.parseComplexJSXElement(MetaJSXElement(
|
|
node=node,
|
|
opening=opening,
|
|
closing=closing,
|
|
children=children
|
|
))
|
|
children = el.children
|
|
closing = el.closing
|
|
|
|
return self.finalize(node, JSXNode.JSXElement(opening, children, closing))
|
|
|
|
def parseJSXRoot(self):
|
|
# Pop the opening '<' added from the lookahead.
|
|
if self.config.tokens:
|
|
self.tokens.pop()
|
|
|
|
self.startJSX()
|
|
element = self.parseJSXElement()
|
|
self.finishJSX()
|
|
|
|
return element
|
|
|
|
def isStartOfExpression(self):
|
|
return super(JSXParser, self).isStartOfExpression() or self.match('<')
|