Angular/angular-backend/src/org/angular2/lang/html/parser/Angular2AttributeNameParser.kt (266 lines of code) (raw):

// Copyright 2000-2020 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.openapi.util.text.StringUtil import com.intellij.psi.xml.XmlTag import com.intellij.xml.util.Html5TagAndAttributeNamesProvider import com.intellij.xml.util.Html5TagAndAttributeNamesProvider.Namespace import com.intellij.xml.util.HtmlUtil import org.angular2.codeInsight.template.isTemplateTag import org.angular2.lang.Angular2Bundle import org.angular2.lang.html.psi.Angular2HtmlEvent import org.angular2.lang.html.psi.Angular2HtmlEvent.AnimationPhase import org.angular2.lang.html.psi.PropertyBindingType import org.angular2.web.ATTR_SELECT import org.angular2.web.ELEMENT_NG_CONTENT import org.angular2.web.ELEMENT_NG_TEMPLATE import org.jetbrains.annotations.Nls object Angular2AttributeNameParser { val ATTR_TO_PROP_MAPPING: Map<String, String> = mapOf( "class" to "className", "for" to "htmlFor", "formaction" to "formAction", "innerHtml" to "innerHTML", "readonly" to "readOnly", "tabindex" to "tabIndex" ) fun parseBound(name: String, tagName: String): AttributeInfo { val info = parse(name) return if (info.type != Angular2AttributeType.REGULAR) info else PropertyBindingInfo(mapToHtmlProp(info.name, tagName), 0, info.isCanonical, false, PropertyBindingType.PROPERTY) } fun parse(name: String): AttributeInfo { return parse(name, ELEMENT_NG_TEMPLATE) } fun parse(name: String, tag: XmlTag?): AttributeInfo { return parse(name, tag?.localName ?: ELEMENT_NG_TEMPLATE) } fun parse(name: String, tagName: String): AttributeInfo { val normalizedName = normalizeAttributeName(name) return when { normalizedName.startsWith("bindon-") -> { parsePropertyBindingCanonical(normalizedName.substring(7), 7, true, tagName) } normalizedName.startsWith("[(") && normalizedName.endsWith(")]") -> { parsePropertyBindingShort(normalizedName.substring(2, normalizedName.length - 2), 2, true, tagName) } normalizedName.startsWith("bind-") -> { parsePropertyBindingCanonical(normalizedName.substring(5), 5, false, tagName) } normalizedName.startsWith("[") && normalizedName.endsWith("]") -> { parsePropertyBindingShort(normalizedName.substring(1, normalizedName.length - 1), 1, false, tagName) } normalizedName.startsWith("on-") -> { parseEvent(normalizedName.substring(3), 3, true) } normalizedName.startsWith("(") && normalizedName.endsWith(")") -> { parseEvent(normalizedName.substring(1, normalizedName.length - 1), 1, false) } normalizedName.startsWith("*") -> { parseTemplateBindings(normalizedName.substring(1), 1) } normalizedName.startsWith("let-") -> { parseLet(normalizedName, normalizedName.substring(4), 4, isTemplateTag(tagName)) } normalizedName.startsWith("#") -> { parseReference(normalizedName, normalizedName.substring(1), 1, false) } normalizedName.startsWith("ref-") -> { parseReference(normalizedName, normalizedName.substring(4), 4, true) } normalizedName.startsWith("@") -> { PropertyBindingInfo(normalizedName.substring(1), 1, false, false, PropertyBindingType.ANIMATION) } normalizedName == ATTR_SELECT && tagName == ELEMENT_NG_CONTENT -> { AttributeInfo(normalizedName, 0, false, Angular2AttributeType.NG_CONTENT_SELECTOR) } normalizedName.startsWith("i18n-") -> { AttributeInfo(normalizedName.substring(5), 5, false, Angular2AttributeType.I18N) } else -> { AttributeInfo(normalizedName, 0, false, Angular2AttributeType.REGULAR) } } } fun normalizeAttributeName(name: String): String { return if (StringUtil.startsWithIgnoreCase(name, HtmlUtil.HTML5_DATA_ATTR_PREFIX)) { name.substring(5) } else name } private fun parsePropertyBindingShort(name: String, nameOffset: Int, bananaBoxBinding: Boolean, tagName: String): AttributeInfo { return when { !bananaBoxBinding && name.startsWith("@") -> { PropertyBindingInfo(name.substring(1), nameOffset + 1, false, false, PropertyBindingType.ANIMATION) } else -> parsePropertyBindingRest(name, nameOffset, false, bananaBoxBinding, tagName) } } private fun parsePropertyBindingCanonical(name: String, nameOffset: Int, bananaBoxBinding: Boolean, tagName: String): AttributeInfo { return when { !bananaBoxBinding && name.startsWith("animate-") -> { PropertyBindingInfo(name.substring(8), nameOffset + 8, true, false, PropertyBindingType.ANIMATION) } else -> parsePropertyBindingRest(name, nameOffset, true, bananaBoxBinding, tagName) } } private fun parsePropertyBindingRest(name: String, nameOffset: Int, isCanonical: Boolean, bananaBoxBinding: Boolean, tagName: String): AttributeInfo { return when { name.startsWith("attr.") -> { PropertyBindingInfo(name.substring(5), nameOffset + 5, isCanonical, bananaBoxBinding, PropertyBindingType.ATTRIBUTE) } name.startsWith("class.") -> { PropertyBindingInfo(name.substring(6), nameOffset + 6, isCanonical, bananaBoxBinding, PropertyBindingType.CLASS) } name.startsWith("style.") -> { PropertyBindingInfo(name.substring(6), nameOffset + 6, isCanonical, bananaBoxBinding, PropertyBindingType.STYLE) } else -> PropertyBindingInfo(mapToHtmlProp(name, tagName), nameOffset, isCanonical, bananaBoxBinding, PropertyBindingType.PROPERTY) } } private fun parseEvent(name: String, nameOffset: Int, isCanonical: Boolean): AttributeInfo { val additionalOffset: Int val eventName = when { name.startsWith("@") -> { additionalOffset = 1 name.substring(1) } name.startsWith("animate-") -> { additionalOffset = 8 name.substring(8) } else -> { return EventInfo(name, nameOffset, isCanonical) } } return parseAnimationEvent(eventName, nameOffset + additionalOffset, isCanonical) } private fun parseTemplateBindings(name: String, nameOffset: Int): AttributeInfo { return AttributeInfo(name, nameOffset, false, Angular2AttributeType.TEMPLATE_BINDINGS) } private fun parseAnimationEvent(name: String, nameOffset: Int, isCanonical: Boolean): AttributeInfo { val dot = name.indexOf('.') if (dot < 0) { return EventInfo(name, nameOffset, isCanonical, AnimationPhase.INVALID, Angular2Bundle.message("angular.parse.template.animation-trigger-missing-phase-value", name)) } val phase = StringUtil.toLowerCase(name.substring(dot + 1)) val eventName = name.substring(0, dot) return when (phase) { "done" -> { EventInfo(eventName, nameOffset, isCanonical, AnimationPhase.DONE) } "start" -> { EventInfo(eventName, nameOffset, isCanonical, AnimationPhase.START) } else -> EventInfo(eventName, nameOffset, isCanonical, AnimationPhase.INVALID, Angular2Bundle.message("angular.parse.template.animation-trigger-wrong-output-phase", phase, eventName.substring(0, dot))) } } private fun parseLet(attrName: String, varName: String, nameOffset: Int, isInTemplateTag: Boolean): AttributeInfo { return when { !isInTemplateTag -> { AttributeInfo(attrName, 0, false, Angular2AttributeType.REGULAR, Angular2Bundle.message("angular.parse.template.let-only-on-ng-template")) } varName.contains("-") -> { AttributeInfo(attrName, 0, false, Angular2AttributeType.REGULAR, Angular2Bundle.message("angular.parse.template.let-dash-not-allowed-in-name")) } varName.isEmpty() -> { AttributeInfo(attrName, 0, false, Angular2AttributeType.REGULAR) } else -> AttributeInfo(varName, nameOffset, false, Angular2AttributeType.LET) } } private fun parseReference(attrName: String, refName: String, nameOffset: Int, isCanonical: Boolean): AttributeInfo { return when { refName.contains("-") -> { AttributeInfo(attrName, 0, false, Angular2AttributeType.REGULAR, Angular2Bundle.message("angular.parse.template.ref-var-dash-not-allowed-in-name")) } refName.isEmpty() -> { AttributeInfo(attrName, 0, false, Angular2AttributeType.REGULAR) } else -> AttributeInfo(refName, nameOffset, isCanonical, Angular2AttributeType.REFERENCE) } } private fun mapToHtmlProp(name: String, tagName: String): String = if (Html5TagAndAttributeNamesProvider.getTags(Namespace.HTML, false).contains(tagName)) ATTR_TO_PROP_MAPPING.getOrDefault(name, name) else name open class AttributeInfo @JvmOverloads constructor(val name: String, val nameOffset: Int, val isCanonical: Boolean, val type: Angular2AttributeType, val error: @Nls String? = null) { open val fullName: String get() = name open fun isEquivalent(otherInfo: AttributeInfo?): Boolean { return otherInfo != null && name == otherInfo.name && type == otherInfo.type } override fun toString(): String { return "<$name>" } } class PropertyBindingInfo( name: String, nameOffset: Int, isCanonical: Boolean, bananaBoxBinding: Boolean, val bindingType: PropertyBindingType, ) : AttributeInfo(name, nameOffset, isCanonical, if (bananaBoxBinding) Angular2AttributeType.BANANA_BOX_BINDING else Angular2AttributeType.PROPERTY_BINDING) { override fun isEquivalent(otherInfo: AttributeInfo?): Boolean { return otherInfo is PropertyBindingInfo && bindingType == otherInfo.bindingType && super.isEquivalent(otherInfo) } override val fullName: String get() = when (bindingType) { PropertyBindingType.ANIMATION -> (if (isCanonical) "animate-" else "@") + name PropertyBindingType.ATTRIBUTE -> "attr.$name" PropertyBindingType.STYLE -> "style.$name" PropertyBindingType.CLASS -> "class.$name" else -> name } override fun toString(): String { return "<$name,$bindingType>" } } class EventInfo : AttributeInfo { val animationPhase: AnimationPhase? val eventType: Angular2HtmlEvent.EventType constructor(name: String, nameOffset: Int, isCanonical: Boolean) : super(name, nameOffset, isCanonical, Angular2AttributeType.EVENT) { eventType = Angular2HtmlEvent.EventType.REGULAR animationPhase = null } @JvmOverloads constructor(name: String, nameOffset: Int, isCanonical: Boolean, animationPhase: AnimationPhase, error: @Nls String? = null) : super(name, nameOffset, isCanonical, Angular2AttributeType.EVENT, error) { this.animationPhase = animationPhase eventType = Angular2HtmlEvent.EventType.ANIMATION } override fun isEquivalent(otherInfo: AttributeInfo?): Boolean { return otherInfo is EventInfo && eventType == otherInfo.eventType && animationPhase == otherInfo.animationPhase && super.isEquivalent(otherInfo) } override val fullName: String get() = if (eventType == Angular2HtmlEvent.EventType.ANIMATION) { if (animationPhase != null) { when (animationPhase) { AnimationPhase.DONE -> "@$name.done" AnimationPhase.START -> "@$name.start" else -> "@$name" } } else "@$name" } else name override fun toString(): String { return "<" + name + ", " + eventType + (if (eventType == Angular2HtmlEvent.EventType.ANIMATION) ", $animationPhase" else "") + ">" } } }