package com.intellij.javascript.flex.mxml; import com.intellij.javascript.flex.FlexPredefinedTagNames; import com.intellij.lang.ASTNode; import com.intellij.lang.Language; import com.intellij.lang.injection.MultiHostInjector; import com.intellij.lang.injection.MultiHostRegistrar; import com.intellij.lang.javascript.JSLanguageInjector; import com.intellij.lang.javascript.JSTargetedInjector; import com.intellij.lang.javascript.JavaScriptSupportLoader; import com.intellij.lang.javascript.flex.AnnotationBackedDescriptor; import com.intellij.lang.javascript.flex.FlexUtils; import com.intellij.lang.javascript.flex.MxmlLanguage; import com.intellij.lang.javascript.flex.XmlBackedJSClassImpl; import com.intellij.lang.javascript.psi.JSCommonTypeNames; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiLanguageInjectionHost; import com.intellij.psi.html.HtmlTag; import com.intellij.psi.meta.PsiMetaData; import com.intellij.psi.xml.*; import com.intellij.xml.XmlElementDescriptorWithCDataContent; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import java.util.Arrays; import java.util.List; public class MxmlLanguageInjector implements MultiHostInjector, JSTargetedInjector { public static final String PRIVATE_TAG_NAME = "Private"; private static final String FUNCTION_CALL_PREFIX = "(function (... _){}) ("; private static final String FUNCTION_CALL_SUFFIX = ");"; private static final Language regexpLanguage = Language.findLanguageByID("RegExp"); private static final Language cssLanguage = Language.findLanguageByID("CSS"); @Override public void getLanguagesToInject(@NotNull MultiHostRegistrar registrar, @NotNull PsiElement host) { if (!host.getContainingFile().getLanguage().isKindOf(MxmlLanguage.INSTANCE)) { return; } if (host instanceof XmlAttributeValue) { final PsiElement attribute = host.getParent(); final PsiElement tag = attribute.getParent(); if (attribute instanceof XmlAttribute && tag instanceof XmlTag) { if (isFxPrivateTag((XmlTag)tag) || isInsideFxPrivateTag((XmlTag)tag)) { return; } if (host.getTextLength() == 0) return; @NonNls String attrName = ((XmlAttribute)attribute).getName(); if ("implements".equals(attrName)) { TextRange range = new TextRange(1, host.getTextLength() - 1); registrar.startInjecting(JavaScriptSupportLoader.ECMA_SCRIPT_L4) .addPlace("class Foo implements ", " {}", (PsiLanguageInjectionHost)host, range) .doneInjecting(); } else if ("source".equals(attrName) && FlexPredefinedTagNames.BINDING.equals(((XmlTag)tag).getLocalName()) && JavaScriptSupportLoader.isLanguageNamespace(((XmlTag)tag).getNamespace()) && !host.textContains('{')) { TextRange range = new TextRange(1, host.getTextLength() - 1); registrar.startInjecting(JavaScriptSupportLoader.ECMA_SCRIPT_L4) .addPlace(FUNCTION_CALL_PREFIX, FUNCTION_CALL_SUFFIX, (PsiLanguageInjectionHost)host, range) .doneInjecting(); } else if (attrName.equals("expression") && "RegExpValidator".equals(((XmlTag)tag).getLocalName()) && regexpLanguage != null ) { String hostText = host.getText(); int startPos = hostText.indexOf('/'); int endPos = hostText.lastIndexOf('/'); if (startPos != -1) { if (endPos > startPos) { TextRange range = new TextRange(startPos + 1, endPos); registrar.startInjecting(regexpLanguage) .addPlace(null, null, (PsiLanguageInjectionHost)host, range) .doneInjecting(); } } else { injectInMxmlFile(registrar, host, ((XmlAttribute)attribute).getDescriptor(), (XmlTag)tag); } } else { injectInMxmlFile(registrar, host, ((XmlAttribute)attribute).getDescriptor(), (XmlTag)tag); } } } else if (host instanceof XmlText) { final PsiElement _tag = host.getParent(); if (_tag instanceof XmlTag) { final XmlTag tag = (XmlTag)_tag; if (isFxPrivateTag(tag) || isInsideFxPrivateTag(tag) || tag instanceof HtmlTag) { return; } final @NonNls String localName = tag.getLocalName(); if ((XmlBackedJSClassImpl.SCRIPT_TAG_NAME.equals(localName) || FlexPredefinedTagNames.METADATA.equals(localName)) && tag.getAttributeValue("source") == null) { JSLanguageInjector.injectToXmlText(registrar, host, JavaScriptSupportLoader.ECMA_SCRIPT_L4, null, null); } else if (FlexPredefinedTagNames.STYLE.equals(localName) && FlexUtils.isMxmlNs(tag.getNamespace()) && cssLanguage != null) { JSLanguageInjector.injectToXmlText(registrar, host, cssLanguage, null, null); } else if (tag.getSubTags().length == 0) { injectInMxmlFile(registrar, host, tag.getDescriptor(), tag); } } } else if (host instanceof XmlComment) { final String text = host.getText(); final String marker = "<!---"; if (text.startsWith(marker)) { final String marker2 = "-->"; int end = text.endsWith(marker2) ? host.getTextLength() - marker2.length() : host.getTextLength(); //int nestedCommentStart = text.indexOf(marker, marker.length()); //if (nestedCommentStart != -1) end = nestedCommentStart; if (end < marker.length()) return; TextRange range = new TextRange(marker.length(), end); registrar.startInjecting(JavaScriptSupportLoader.ECMA_SCRIPT_L4) .addPlace("/***", "*/", (PsiLanguageInjectionHost)host, range) .doneInjecting(); } } } @NotNull @Override public List<? extends Class<? extends PsiElement>> elementsToInjectIn() { return Arrays.asList(XmlText.class, XmlAttributeValue.class, XmlComment.class); } public static boolean isFxPrivateTag(final XmlTag tag) { return tag != null && PRIVATE_TAG_NAME.equals(tag.getLocalName()) && JavaScriptSupportLoader.MXML_URI3.equals(tag.getNamespace()); } public static boolean isInsideFxPrivateTag(final XmlTag tag) { if (tag == null) return false; XmlTag parent = tag; while ((parent = parent.getParentTag()) != null) { if (isFxPrivateTag(parent)) { return true; } } return false; } private static void injectInMxmlFile(final MultiHostRegistrar registrar, final PsiElement host, final PsiMetaData descriptor, XmlTag tag) { int offset = host instanceof XmlText ? 0 : 1; if (descriptor instanceof AnnotationBackedDescriptor && ((XmlElementDescriptorWithCDataContent)descriptor).requiresCdataBracesInContext(tag)) { final int length = host.getTextLength(); if (length < 2 * offset) return; String type = ((AnnotationBackedDescriptor)descriptor).getType(); if (type == null) type = "*"; @NonNls String prefix = "(function (event:" + type + ") {"; @NonNls String suffix = "})(null);"; if (host instanceof XmlText) { JSLanguageInjector.injectToXmlText(registrar, host, JavaScriptSupportLoader.ECMA_SCRIPT_L4, prefix, suffix); } else { if (JSCommonTypeNames.FUNCTION_CLASS_NAME.equals(type) && host.textContains('{')) { final String text = StringUtil.stripQuotesAroundValue(host.getText()); if (text.startsWith("{") && text.endsWith("}")) { prefix = FUNCTION_CALL_PREFIX; suffix = FUNCTION_CALL_SUFFIX; offset++; } } TextRange range = new TextRange(offset, length - offset); registrar.startInjecting(JavaScriptSupportLoader.ECMA_SCRIPT_L4) .addPlace(prefix, (host instanceof XmlAttributeValue ? "\n" : "") + suffix, (PsiLanguageInjectionHost)host, range) .doneInjecting(); } } else if (!(host instanceof XmlText) || !hasCDATA((XmlText)host)){ final String text = StringUtil.stripQuotesAroundValue(host.getText()); int openedBraces = 0; int start = -1; boolean addedSomething = false; boolean quoted = false; for (int i = 0; i < text.length(); ++i) { final char ch = text.charAt(i); if (quoted) { quoted = false; continue; } if (ch == '\\') { quoted = true; } else if (ch == '{') { if (openedBraces == 0) start = i + 1; openedBraces++; } else if (ch == '}') { openedBraces--; if (openedBraces == 0 && start != -1) { registrar.startInjecting(JavaScriptSupportLoader.ECMA_SCRIPT_L4) .addPlace(FUNCTION_CALL_PREFIX, FUNCTION_CALL_SUFFIX, (PsiLanguageInjectionHost)host, new TextRange(offset + start, i + offset)) .doneInjecting(); addedSomething = true; start = -1; } } } if (!addedSomething) { final String trimmedText = text.trim(); start = trimmedText.indexOf("@"); if (start == 0 && trimmedText.length() > 1 && Character.isUpperCase(trimmedText.charAt(1))) { // @id can be reference to attribute offset += text.indexOf(trimmedText); registrar.startInjecting(JavaScriptSupportLoader.ECMA_SCRIPT_L4) .addPlace(null, null, (PsiLanguageInjectionHost)host, new TextRange(offset, trimmedText.length() + offset)) .doneInjecting(); } } } } private static boolean hasCDATA(final XmlText xmlText) { for (PsiElement element : xmlText.getChildren()) { final ASTNode node = element.getNode(); if (node != null && node.getElementType() == XmlElementType.XML_CDATA) { return true; } } return false; } }