Angular/angular-backend/src/org/angular2/lang/html/parser/Angular2HtmlParsing.kt (570 lines of code) (raw):
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package org.angular2.lang.html.parser
import com.intellij.lang.PsiBuilder
import com.intellij.lang.PsiBuilder.Marker
import com.intellij.lang.html.HtmlParsing
import com.intellij.lang.javascript.JavaScriptParserBundle
import com.intellij.psi.tree.ICustomParsingType
import com.intellij.psi.tree.IElementType
import com.intellij.psi.tree.ILazyParseableElementType
import com.intellij.psi.tree.TokenSet
import com.intellij.psi.xml.XmlElementType
import com.intellij.psi.xml.XmlTokenType
import com.intellij.xml.parsing.XmlParserBundle
import com.intellij.xml.util.XmlUtil
import org.angular2.codeInsight.blocks.BLOCK_LET
import org.angular2.lang.Angular2Bundle
import org.angular2.lang.expr.parser.Angular2EmbeddedExprTokenType
import org.angular2.lang.expr.parser.Angular2EmbeddedExprTokenType.Angular2InterpolationExprTokenType
import org.angular2.lang.expr.parser.Angular2EmbeddedExprTokenType.Companion.createTemplateBindings
import org.angular2.lang.html.Angular2TemplateSyntax
import org.angular2.lang.html.lexer.Angular2HtmlTokenTypes
import org.angular2.web.ATTR_NG_NON_BINDABLE
open class Angular2HtmlParsing(private val templateSyntax: Angular2TemplateSyntax, builder: PsiBuilder) : HtmlParsing(builder) {
fun parseExpansionFormContent() {
val expansionFormContent = mark()
var xmlText: Marker? = null
while (!eof()) {
when (token()) {
XmlTokenType.XML_START_TAG_START -> {
xmlText = terminateText(xmlText)
parseTag()
flushIncompleteStackItemsWhile { it is HtmlTagInfo }
}
else -> {
xmlText = defaultParseTopLevelTokenWithText(xmlText)
}
}
}
terminateText(xmlText)
expansionFormContent.done(Angular2HtmlElementTypes.EXPANSION_FORM_CASE_CONTENT)
}
override fun hasCustomTopLevelContent(): Boolean {
return CUSTOM_CONTENT.contains(token())
}
override fun hasCustomTagContent(): Boolean {
return CUSTOM_CONTENT.contains(token())
}
private fun parseTopLevelBlock() {
parseBlockStart()
if (stackSize() == 0) return
var xmlText: Marker? = null
while (!eof()) {
when (token()) {
XmlTokenType.XML_START_TAG_START -> {
xmlText = terminateText(xmlText)
parseTag()
}
Angular2HtmlTokenTypes.BLOCK_END -> {
xmlText = parseCustomTagContent(xmlText)
if (!hasAngularBlockOnStack) {
break
}
}
else -> {
xmlText = defaultParseTopLevelTokenWithText(xmlText)
}
}
}
terminateText(xmlText)
}
private fun defaultParseTopLevelTokenWithText(xmlText: Marker?): Marker? {
var xmlText = xmlText
when (token()) {
XmlTokenType.XML_PI_START -> {
xmlText = terminateText(xmlText)
parseProcessingInstruction()
}
XmlTokenType.XML_CHAR_ENTITY_REF, XmlTokenType.XML_ENTITY_REF_TOKEN -> {
xmlText = startText(xmlText)
parseReference()
}
XmlTokenType.XML_CDATA_START -> {
xmlText = startText(xmlText)
parseCData()
}
XmlTokenType.XML_COMMENT_START -> {
xmlText = startText(xmlText)
parseComment()
}
XmlTokenType.XML_BAD_CHARACTER -> {
xmlText = startText(xmlText)
val error = mark()
advance()
error.error(XmlParserBundle.message("xml.parsing.unescaped.ampersand.or.nonterminated.character.entity.reference"))
}
XmlTokenType.XML_END_TAG_START -> {
val tagEndError = mark()
advance()
if (token() === XmlTokenType.XML_NAME) {
advance()
if (token() === XmlTokenType.XML_TAG_END) {
advance()
}
}
tagEndError.error(XmlParserBundle.message("xml.parsing.closing.tag.matches.nothing"))
}
is ICustomParsingType, is ILazyParseableElementType -> {
xmlText = terminateText(xmlText)
advance()
}
else -> {
if (hasCustomTagContent()) {
xmlText = parseCustomTagContent(xmlText)
}
else {
xmlText = startText(xmlText)
advance()
}
}
}
return xmlText
}
private val hasAngularBlockOnStack: Boolean
get() {
var result: AngularBlock? = null
processStackItems {
if (it is AngularBlock) {
result = it
false
}
else true
}
return result != null
}
override fun parseCustomTagContent(xmlText: Marker?): Marker? {
var result = xmlText
when (token()) {
Angular2HtmlTokenTypes.INTERPOLATION_START -> {
result = if (!inNgNonBindableContext()) {
terminateText(result)
}
else {
startText(result)
}
val interpolation = mark()
advance()
if (token() is Angular2InterpolationExprTokenType) {
advance()
}
if (!inNgNonBindableContext()) {
if (token() === Angular2HtmlTokenTypes.INTERPOLATION_END) {
advance()
interpolation.drop()
}
else {
interpolation.error(Angular2Bundle.message("angular.parse.template.unterminated-interpolation"))
}
}
else {
if (token() === Angular2HtmlTokenTypes.INTERPOLATION_END) {
advance()
}
interpolation.collapse(XmlTokenType.XML_DATA_CHARACTERS)
}
}
Angular2HtmlTokenTypes.EXPANSION_FORM_START -> {
result = terminateText(result)
parseExpansionForm()
}
XmlTokenType.XML_COMMA -> {
result = startText(result)
builder.remapCurrentToken(XmlTokenType.XML_DATA_CHARACTERS)
advance()
}
XmlTokenType.XML_DATA_CHARACTERS -> {
result = startText(result)
val dataStart = mark()
while (DATA_TOKENS.contains(token())) {
advance()
}
dataStart.collapse(XmlTokenType.XML_DATA_CHARACTERS)
}
Angular2HtmlTokenTypes.BLOCK_NAME -> {
result = terminateText(result)
parseBlockStart()
}
Angular2HtmlTokenTypes.BLOCK_END -> {
result = terminateText(result)
if (hasAngularBlockOnStack) {
flushIncompleteStackItemsWhile { it !is AngularBlock }
advance()
completeTopStackItem()
}
else {
builder.error(Angular2Bundle.message("angular.parse.template.unexpected-block-closing-rbrace"))
advance()
}
}
}
return result
}
private fun parseBlockStart() {
assert(builder.tokenType == Angular2HtmlTokenTypes.BLOCK_NAME)
val startMarker = builder.mark()
val blockName = builder.tokenText!!.removePrefix("@")
builder.advanceLexer()
if (blockName == "") {
startMarker.done(Angular2HtmlElementTypes.BLOCK)
return
}
else if (blockName == BLOCK_LET) {
parseLetBlock(startMarker)
return
}
if (builder.tokenType == Angular2HtmlTokenTypes.BLOCK_PARAMETERS_START) {
val parameters = builder.mark()
builder.advanceLexer()
val parametersContents = builder.mark()
var parameterIndex = 0
while (!builder.eof()) {
if (builder.tokenType is Angular2EmbeddedExprTokenType) {
builder.advanceLexer()
if (builder.tokenType == Angular2HtmlTokenTypes.BLOCK_SEMICOLON) {
builder.advanceLexer()
}
}
else if (builder.tokenType == Angular2HtmlTokenTypes.BLOCK_SEMICOLON) {
builder.mark().collapse(Angular2EmbeddedExprTokenType.createBlockParameter(templateSyntax, blockName, parameterIndex))
builder.advanceLexer()
}
else {
break
}
parameterIndex++
}
if (builder.eof() || builder.tokenType != Angular2HtmlTokenTypes.BLOCK_PARAMETERS_END) {
parameters.errorBefore(JavaScriptParserBundle.message("javascript.parser.message.missing.rparen"), parametersContents)
parametersContents.drop()
parameters.precede().done(Angular2HtmlElementTypes.BLOCK_PARAMETERS)
}
else {
builder.advanceLexer()
parametersContents.drop()
parameters.done(Angular2HtmlElementTypes.BLOCK_PARAMETERS)
}
}
if (builder.tokenType == Angular2HtmlTokenTypes.BLOCK_START) {
val errorStartMarker = builder.mark()
builder.advanceLexer()
val errorEndMarker = builder.mark()
pushItemToStack(AngularBlock(startMarker, errorStartMarker.precede(), errorStartMarker, errorEndMarker))
}
else {
builder.error(Angular2Bundle.message("angular.parse.template.missing-block-opening-lbrace"))
startMarker.done(Angular2HtmlElementTypes.BLOCK)
}
}
private fun parseLetBlock(startMarker: Marker) {
if (builder.tokenType !is Angular2EmbeddedExprTokenType) {
if (builder.rawLookup(-1) == Angular2HtmlTokenTypes.BLOCK_NAME) {
builder.error(Angular2Bundle.message("angular.parse.expression.expected-whitespace"))
}
else {
builder.error(JavaScriptParserBundle.message("javascript.parser.message.expected.identifier"))
}
startMarker.done(Angular2HtmlElementTypes.BLOCK)
return
}
val parameters = builder.mark()
builder.advanceLexer()
if (builder.tokenType != Angular2HtmlTokenTypes.BLOCK_SEMICOLON) {
builder.error(Angular2Bundle.message("angular.parse.template.missing-let-block-closing-semicolon"))
}
else {
builder.advanceLexer()
}
parameters.done(Angular2HtmlElementTypes.BLOCK_PARAMETERS)
startMarker.done(Angular2HtmlElementTypes.BLOCK)
}
override fun parseCustomTopLevelContent(error: Marker?): Marker? {
val result = flushError(error)
if (token() == Angular2HtmlTokenTypes.BLOCK_NAME)
parseTopLevelBlock()
else
terminateText(parseCustomTagContent(null))
return result
}
override fun createHtmlTagInfo(originalTagName: String, startMarker: Marker): HtmlTagInfoImpl {
return AngularHtmlTagInfo(normalizeTagName(originalTagName), originalTagName, startMarker)
}
override fun parseAttribute() {
assert(token() === XmlTokenType.XML_NAME)
val att = mark()
val tagName = XmlUtil.findLocalNameByQualifiedName(peekTagInfo().normalizedName)
val attributeName = builder.tokenText
if (ATTR_NG_NON_BINDABLE == attributeName) {
(peekTagInfo() as AngularHtmlTagInfo).hasNgNonBindable = true
}
val attributeInfo = Angular2AttributeNameParser.parse(attributeName!!, tagName!!)
if (attributeInfo.error != null) {
val attrName = mark()
advance()
attrName.error(attributeInfo.error)
}
else if (attributeInfo.type == Angular2AttributeType.REFERENCE) {
val attrName = mark()
advance()
attrName.collapse(Angular2HtmlVarAttrTokenType.REFERENCE)
}
else if (attributeInfo.type == Angular2AttributeType.LET) {
val attrName = mark()
advance()
attrName.collapse(Angular2HtmlVarAttrTokenType.LET)
}
else {
advance()
}
var attributeElementType = attributeInfo.type.elementType
if (token() === XmlTokenType.XML_EQ) {
advance()
attributeElementType = parseAttributeValue(attributeElementType, attributeInfo.name)
}
att.done(
if (attributeElementType !== Angular2HtmlElementTypes.NG_CONTENT_SELECTOR)
attributeElementType
else
XmlElementType.XML_ATTRIBUTE
)
}
private fun parseAttributeValue(attributeElementType: IElementType, name: String): IElementType {
var result = attributeElementType
val attValue = mark()
val contentType = getAttributeContentType(templateSyntax, result, name)
if (token() === XmlTokenType.XML_ATTRIBUTE_VALUE_START_DELIMITER) {
advance()
val contentStart = if (contentType != null) mark() else null
while (true) {
val tt = token()
if (tt == null
|| tt === XmlTokenType.XML_ATTRIBUTE_VALUE_END_DELIMITER
|| tt === XmlTokenType.XML_END_TAG_START
|| tt === XmlTokenType.XML_EMPTY_ELEMENT_END
|| tt === XmlTokenType.XML_START_TAG_START) {
break
}
if (tt is Angular2InterpolationExprTokenType && result === XmlElementType.XML_ATTRIBUTE) {
result = Angular2HtmlElementTypes.PROPERTY_BINDING
}
when (tt) {
XmlTokenType.XML_BAD_CHARACTER -> {
val error = mark()
advance()
error.error(XmlParserBundle.message("xml.parsing.unescaped.ampersand.or.nonterminated.character.entity.reference"))
}
XmlTokenType.XML_ENTITY_REF_TOKEN -> {
parseReference()
}
else -> {
advance()
}
}
}
if (contentStart != null) {
if (contentType === Angular2HtmlElementTypes.NG_CONTENT_SELECTOR) {
contentStart.done(contentType)
}
else {
contentStart.collapse(contentType!!)
}
}
if (token() === XmlTokenType.XML_ATTRIBUTE_VALUE_END_DELIMITER) {
advance()
}
else {
error(XmlParserBundle.message("xml.parsing.unclosed.attribute.value"))
}
}
else {
if (token().let { it !== XmlTokenType.XML_TAG_END && it !== XmlTokenType.XML_EMPTY_ELEMENT_END }) {
if (contentType != null) {
val contentStart = mark()
advance()
if (contentType === Angular2HtmlElementTypes.NG_CONTENT_SELECTOR) {
contentStart.done(contentType)
}
else {
contentStart.collapse(contentType)
}
}
else {
advance() // Single token att value
}
}
}
attValue.done(XmlElementType.XML_ATTRIBUTE_VALUE)
return result
}
private fun parseExpansionForm() {
assert(token() === Angular2HtmlTokenTypes.EXPANSION_FORM_START)
var expansionForm = mark()
advance()
if (!remapTokensUntilComma(Angular2EmbeddedExprTokenType.createBindingExpr(templateSyntax)) /*switch value*/
|| !remapTokensUntilComma(XmlTokenType.XML_DATA_CHARACTERS) /*type*/) {
markCriticalExpansionFormProblem(expansionForm)
return
}
skipRealWhiteSpaces()
var first = true
while (token().let { it === XmlTokenType.XML_DATA_CHARACTERS || it === Angular2HtmlTokenTypes.EXPANSION_FORM_CASE_START }) {
if (!parseExpansionFormCaseContent() && first) {
markCriticalExpansionFormProblem(expansionForm)
return
}
first = false
skipRealWhiteSpaces()
}
if (token() !== Angular2HtmlTokenTypes.EXPANSION_FORM_END) {
expansionForm
.error(Angular2Bundle.message("angular.parse.template.unterminated-expansion-form"))
expansionForm = expansionForm.precede()
}
else {
advance()
}
expansionForm.done(Angular2HtmlElementTypes.EXPANSION_FORM)
}
private fun markCriticalExpansionFormProblem(expansionForm: Marker) {
// critical problem, most likely not an expansion form at all
expansionForm.rollbackTo()
val errorMarker = mark()
assert(token() === Angular2HtmlTokenTypes.EXPANSION_FORM_START)
advance() //consume LBRACE
errorMarker.error(Angular2Bundle.message("angular.parse.template.unterminated-expansion-form"))
}
private fun remapTokensUntilComma(textType: IElementType): Boolean {
val start = mark()
while (!eof() && token() !== XmlTokenType.XML_COMMA) {
advance()
}
start.collapse(textType)
if (token() !== XmlTokenType.XML_COMMA) {
start.precede().error(Angular2Bundle.message("angular.parse.template.invalid-icu-message-expected-comma"))
return false
}
advance()
return true
}
private fun parseExpansionFormCaseContent(): Boolean {
var expansionFormCase = mark()
if (token() === XmlTokenType.XML_DATA_CHARACTERS) {
advance() // value
skipRealWhiteSpaces()
if (token() !== Angular2HtmlTokenTypes.EXPANSION_FORM_CASE_START) {
expansionFormCase.error(Angular2Bundle.message("angular.parse.template.invalid-icu-message-expected-left-brace"))
expansionFormCase.precede().done(Angular2HtmlElementTypes.EXPANSION_FORM_CASE)
return false
}
}
else if (token() === Angular2HtmlTokenTypes.EXPANSION_FORM_CASE_START) {
advance()
expansionFormCase.error(Angular2Bundle.message("angular.parse.template.invalid-icu-message-missing-case-value"))
expansionFormCase = expansionFormCase.precede()
}
else {
throw IllegalStateException()
}
advance()
val content = mark()
var level = 1
var tt: IElementType?
while (token().also { tt = it } !== Angular2HtmlTokenTypes.EXPANSION_FORM_CASE_END || level > 1) {
when (tt) {
Angular2HtmlTokenTypes.EXPANSION_FORM_CASE_START -> {
level++
}
Angular2HtmlTokenTypes.EXPANSION_FORM_CASE_END -> {
level--
}
null -> {
content.error(Angular2Bundle.message("angular.parse.template.invalid-icu-message-missing-right-brace"))
expansionFormCase.done(Angular2HtmlElementTypes.EXPANSION_FORM_CASE)
return false
}
}
advance()
}
content.collapse(Angular2ExpansionFormCaseContentTokenType.get(templateSyntax))
advance()
expansionFormCase.done(Angular2HtmlElementTypes.EXPANSION_FORM_CASE)
return true
}
private fun skipRealWhiteSpaces() {
while (token() === XmlTokenType.XML_REAL_WHITE_SPACE) {
advance()
}
}
private fun inNgNonBindableContext(): Boolean {
var result = false
processStackItems {
if (it is AngularHtmlTagInfo && it.hasNgNonBindable) {
result = true
false
}
else true
}
return result
}
private inner class AngularHtmlTagInfo(
normalizedName: String,
originalName: String,
marker: Marker,
var hasNgNonBindable: Boolean = false,
) : HtmlTagInfoImpl(normalizedName, originalName, marker)
private class AngularBlock(
private val startMarker: Marker,
private val contentsMarker: Marker,
private val errorStartMarker: Marker,
private val errorEndMarker: Marker,
) : HtmlParserStackItem {
override fun done(
builder: PsiBuilder,
beforeMarker: Marker?,
incomplete: Boolean,
) {
if (incomplete) {
errorStartMarker.errorBefore(Angular2Bundle.message("angular.parse.template.missing-block-closing-rbrace"), errorEndMarker)
}
else {
errorStartMarker.drop()
}
errorEndMarker.drop()
if (beforeMarker == null) {
contentsMarker.done(Angular2HtmlElementTypes.BLOCK_CONTENTS)
startMarker.done(Angular2HtmlElementTypes.BLOCK)
}
else {
contentsMarker.doneBefore(Angular2HtmlElementTypes.BLOCK_CONTENTS, beforeMarker)
startMarker.doneBefore(Angular2HtmlElementTypes.BLOCK, beforeMarker)
}
}
}
companion object {
private val CUSTOM_CONTENT = TokenSet.create(Angular2HtmlTokenTypes.EXPANSION_FORM_START,
Angular2HtmlTokenTypes.INTERPOLATION_START,
XmlTokenType.XML_DATA_CHARACTERS,
XmlTokenType.XML_COMMA,
Angular2HtmlTokenTypes.BLOCK_NAME,
Angular2HtmlTokenTypes.BLOCK_END)
private val DATA_TOKENS = TokenSet.create(XmlTokenType.XML_COMMA, XmlTokenType.XML_DATA_CHARACTERS)
private fun getAttributeContentType(templateSyntax: Angular2TemplateSyntax, type: IElementType, name: String): IElementType? =
when (type) {
Angular2HtmlElementTypes.PROPERTY_BINDING, Angular2HtmlElementTypes.BANANA_BOX_BINDING -> {
Angular2EmbeddedExprTokenType.createBindingExpr(templateSyntax)
}
Angular2HtmlElementTypes.EVENT -> {
Angular2EmbeddedExprTokenType.createActionExpr(templateSyntax)
}
Angular2HtmlElementTypes.TEMPLATE_BINDINGS -> {
createTemplateBindings(templateSyntax, name)
}
Angular2HtmlElementTypes.NG_CONTENT_SELECTOR -> {
Angular2HtmlElementTypes.NG_CONTENT_SELECTOR
}
Angular2HtmlElementTypes.REFERENCE, Angular2HtmlElementTypes.LET, XmlElementType.XML_ATTRIBUTE -> {
null
}
else -> {
throw IllegalStateException("Unsupported element type: $type")
}
}
}
}