package com.intellij.javascript.flex.mxml.schema;
import com.intellij.codeInsight.daemon.IdeValidationHost;
import com.intellij.codeInsight.daemon.Validator;
import com.intellij.codeInsight.daemon.XmlErrorMessages;
import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.javascript.flex.FlexPredefinedTagNames;
import com.intellij.javascript.flex.FlexStateElementNames;
import com.intellij.javascript.flex.mxml.MxmlJSClass;
import com.intellij.javascript.flex.mxml.MxmlLanguageInjector;
import com.intellij.lang.ASTNode;
import com.intellij.lang.javascript.JSBundle;
import com.intellij.lang.javascript.flex.FlexBundle;
import com.intellij.lang.javascript.flex.XmlBackedJSClassImpl;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiWhiteSpace;
import com.intellij.psi.impl.source.xml.XmlAttributeImpl;
import com.intellij.psi.xml.*;
import com.intellij.util.ArrayUtil;
import com.intellij.util.IncorrectOperationException;
import com.intellij.xml.XmlNamespaceHelper;
import com.intellij.xml.util.XmlTagUtil;
import com.intellij.xml.util.XmlUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collections;
import java.util.Set;
import static com.intellij.lang.javascript.JavaScriptSupportLoader.*;
public class MxmlLanguageTagsUtil {
static final String NAME_ATTRIBUTE = "name";
private static final String[] LANGUAGE_TAGS_ALLOWED_UNDER_ROOT_TAG = {
FlexPredefinedTagNames.BINDING,
FlexPredefinedTagNames.DECLARATIONS,
FlexPredefinedTagNames.LIBRARY,
FlexPredefinedTagNames.METADATA,
MxmlLanguageInjector.PRIVATE_TAG_NAME,
FlexPredefinedTagNames.SCRIPT,
FlexPredefinedTagNames.STYLE
};
private static final String[] LANGUAGE_TAGS_ALLOWED_UNDER_INLINE_COMPONENT_ROOT_TAG = {
FlexPredefinedTagNames.BINDING,
FlexPredefinedTagNames.DECLARATIONS,
FlexPredefinedTagNames.METADATA,
FlexPredefinedTagNames.SCRIPT,
FlexPredefinedTagNames.STYLE
};
private MxmlLanguageTagsUtil() {
}
static boolean isComponentTag(final XmlTag tag) {
return tag != null && XmlBackedJSClassImpl.isComponentTag(tag);
}
static boolean isFxPrivateTag(final XmlTag tag) {
return MxmlLanguageInjector.isFxPrivateTag(tag);
}
static boolean isXmlOrXmlListTag(final XmlTag tag) {
return tag != null &&
(MxmlJSClass.XML_TAG_NAME.equals(tag.getLocalName()) ||
MxmlJSClass.XMLLIST_TAG_NAME.equals(tag.getLocalName())) &&
(isLanguageNamespace(tag.getNamespace()));
}
static boolean isFxLibraryTag(final XmlTag tag) {
return MxmlJSClass.isFxLibraryTag(tag);
}
static boolean isFxDefinitionTag(final XmlTag tag) {
return tag != null && CodeContext.DEFINITION_TAG_NAME.equals(tag.getLocalName()) && MXML_URI3.equals(tag.getNamespace());
}
static boolean isFxDeclarationsTag(final XmlTag tag) {
return tag != null && FlexPredefinedTagNames.DECLARATIONS.equals(tag.getLocalName()) && MXML_URI3.equals(tag.getNamespace());
}
public static boolean isFxReparentTag(final XmlTag tag) {
return tag != null && CodeContext.REPARENT_TAG_NAME.equals(tag.getLocalName()) && MXML_URI3.equals(tag.getNamespace());
}
public static boolean isScriptTag(final XmlTag tag) {
return tag != null && FlexPredefinedTagNames.SCRIPT.equals(tag.getLocalName()) && isLanguageNamespace(tag.getNamespace());
}
public static boolean isDesignLayerTag(final XmlTag tag) {
return tag != null && FlexPredefinedTagNames.DESIGN_LAYER.equals(tag.getLocalName()) && MXML_URI3.equals(tag.getNamespace());
}
public static boolean isLanguageTagAllowedUnderRootTag(final XmlTag tag) {
return tag != null &&
(MXML_URI3.equals(tag.getNamespace()) || MXML_URI.equals(tag.getNamespace())) &&
ArrayUtil.contains(tag.getLocalName(), LANGUAGE_TAGS_ALLOWED_UNDER_ROOT_TAG);
}
public static boolean isLanguageTagAllowedUnderInlineComponentRootTag(final XmlTag tag) {
return tag != null &&
(MXML_URI3.equals(tag.getNamespace()) || MXML_URI.equals(tag.getNamespace())) &&
ArrayUtil.contains(tag.getLocalName(), LANGUAGE_TAGS_ALLOWED_UNDER_INLINE_COMPONENT_ROOT_TAG);
}
static void validateFxPrivateTag(final XmlTag tag, final Validator.ValidationHost host) {
final XmlTag parentTag = tag.getParentTag();
if (parentTag == null ||
!(parentTag.getParent() instanceof XmlDocument) ||
tag != parentTag.getSubTags()[parentTag.getSubTags().length - 1]) {
addErrorMessage(tag, JSBundle.message("javascript.validation.tag.must.be.last.child.of.root.tag", tag.getName()), host);
//return;
}
}
static void validateFxLibraryTag(final XmlTag tag, final Validator.ValidationHost host) {
final XmlTag parentTag = tag.getParentTag();
if (parentTag == null || !(parentTag.getParent() instanceof XmlDocument) || tag != parentTag.getSubTags()[0]) {
addErrorMessage(tag, JSBundle.message("javascript.validation.tag.must.be.first.child.of.root.tag", tag.getName()), host);
return;
}
for (XmlAttribute attribute : tag.getAttributes()) {
addErrorMessage(attribute, XmlErrorMessages.message("attribute.is.not.allowed.here", attribute.getName()), host);
}
for (XmlTag subTag : tag.getSubTags()) {
if (!isFxDefinitionTag(subTag)) {
final String prefix = tag.getNamespacePrefix();
final String fxDefinitionTag =
StringUtil.isEmpty(prefix) ? CodeContext.DEFINITION_TAG_NAME : (prefix + ":" + CodeContext.DEFINITION_TAG_NAME);
addErrorMessage(subTag, JSBundle.message("javascript.validation.only.this.tag.is.allowed.here", fxDefinitionTag), host);
}
}
}
static void validateFxDefinitionTag(final XmlTag tag, final Validator.ValidationHost host) {
final XmlTag parentTag = tag.getParentTag();
if (!isFxLibraryTag(parentTag)) {
final String prefix = tag.getNamespacePrefix();
final String fxLibraryTag = StringUtil.isEmpty(prefix) ? FlexPredefinedTagNames.LIBRARY
: (prefix + ":" + FlexPredefinedTagNames.LIBRARY);
addErrorMessage(tag,
JSBundle.message("javascript.validation.tag.must.be.direct.child.of.fx.library.tag", tag.getName(), fxLibraryTag),
host);
return;
}
if (tag.getAttribute(NAME_ATTRIBUTE) == null) {
addErrorMessage(tag, XmlErrorMessages.message("element.doesnt.have.required.attribute", tag.getName(), NAME_ATTRIBUTE), host);
return;
}
if (tag.getSubTags().length != 1) {
addErrorMessage(tag, JSBundle.message("javascript.validation.tag.must.have.exactly.one.child.tag", tag.getName()), host);
//return;
}
}
static void validateFxReparentTag(final XmlTag tag, final Validator.ValidationHost host) {
if (tag.getAttribute(CodeContext.TARGET_ATTR_NAME) == null) {
addErrorMessage(tag, XmlErrorMessages.message("element.doesnt.have.required.attribute", tag.getName(), CodeContext.TARGET_ATTR_NAME),
host);
return;
}
if (tag.getAttribute(FlexStateElementNames.INCLUDE_IN) == null &&
tag.getAttribute(FlexStateElementNames.EXCLUDE_FROM) == null) {
addErrorMessage(tag, JSBundle.message("javascript.validation.tag.must.have.attribute.includein.or.excludefrom", tag.getName()), host);
//return;
}
}
public static void checkFlex4Attributes(@NotNull final XmlTag tag,
@NotNull final Validator.ValidationHost host,
final boolean checkStateSpecificAttrs) {
XmlAttribute flex3NamespaceDeclaration = null;
XmlAttribute flex4NamespaceDeclaration = null;
XmlAttribute itemCreationPolicyAttr = null;
XmlAttribute itemDestructionPolicyAttr = null;
boolean includeInOrExcludeFromAttrPresent = false;
for (final XmlAttribute attribute : tag.getAttributes()) {
final String name = attribute.getName();
if (attribute.isNamespaceDeclaration()) {
final String namespace = attribute.getValue();
if (MXML_URI.equals(namespace)) {
flex3NamespaceDeclaration = attribute;
}
else if (MXML_URI3.equals(namespace)) {
flex4NamespaceDeclaration = attribute;
}
}
else if (checkStateSpecificAttrs) {
if (FlexStateElementNames.INCLUDE_IN.equals(name) ||
FlexStateElementNames.EXCLUDE_FROM.equals(name)) {
includeInOrExcludeFromAttrPresent = true;
}
else if (FlexStateElementNames.ITEM_CREATION_POLICY.equals(name)) {
itemCreationPolicyAttr = attribute;
}
else if (FlexStateElementNames.ITEM_DESTRUCTION_POLICY.equals(name)) {
itemDestructionPolicyAttr = attribute;
}
}
}
if (tag.getParent() instanceof XmlDocument) {
if (flex3NamespaceDeclaration == null && flex4NamespaceDeclaration == null) {
final String[] knownNamespaces = tag.knownNamespaces();
boolean suggestFlex3Namespace = true;
for (final String flex4Namespace : MxmlJSClass.FLEX_4_NAMESPACES) {
if (ArrayUtil.contains(flex4Namespace, knownNamespaces)) {
suggestFlex3Namespace = false;
break;
}
}
final DeclareNamespaceIntention flex4Intention = new DeclareNamespaceIntention(tag, "fx", MXML_URI3);
final IntentionAction[] intentions = suggestFlex3Namespace ? new IntentionAction[]{flex4Intention,
new DeclareNamespaceIntention(tag, "mx", MXML_URI)} : new IntentionAction[]{flex4Intention};
addErrorMessage(tag, FlexBundle.message("root.tag.must.contain.language.namespace"), host, intentions);
}
else if (flex3NamespaceDeclaration != null && flex4NamespaceDeclaration != null) {
addErrorMessage(flex3NamespaceDeclaration.getValueElement(), FlexBundle.message("different.language.namespaces"), host,
new RemoveNamespaceDeclarationIntention(flex3NamespaceDeclaration));
addErrorMessage(flex4NamespaceDeclaration.getValueElement(), FlexBundle.message("different.language.namespaces"), host,
new RemoveNamespaceDeclarationIntention(flex4NamespaceDeclaration));
}
}
else {
final String[] knownNamespaces = tag.knownNamespaces();
if (flex3NamespaceDeclaration != null && ArrayUtil.contains(MXML_URI3, knownNamespaces)) {
addErrorMessage(flex3NamespaceDeclaration.getValueElement(), FlexBundle.message("different.language.namespaces"), host,
new RemoveNamespaceDeclarationIntention(flex3NamespaceDeclaration));
}
if (flex4NamespaceDeclaration != null && ArrayUtil.contains(MXML_URI, knownNamespaces)) {
addErrorMessage(flex4NamespaceDeclaration.getValueElement(), FlexBundle.message("different.language.namespaces"), host,
new RemoveNamespaceDeclarationIntention(flex4NamespaceDeclaration));
}
}
if (checkStateSpecificAttrs && !includeInOrExcludeFromAttrPresent) {
if (itemCreationPolicyAttr != null) {
addErrorMessage(itemCreationPolicyAttr,
FlexBundle.message("must.accompany.includein.or.excludefrom.attribute", itemCreationPolicyAttr.getName()), host);
}
if (itemDestructionPolicyAttr != null) {
addErrorMessage(itemDestructionPolicyAttr,
FlexBundle.message("must.accompany.includein.or.excludefrom.attribute", itemDestructionPolicyAttr.getName()), host);
}
}
}
private static void addErrorMessage(final XmlElement element,
final String message,
final Validator.ValidationHost host,
@NotNull IntentionAction... intentionActions) {
PsiElement target = element;
PsiElement secondaryTarget = null;
if (element instanceof XmlAttributeValue) {
final ASTNode node = element.getNode();
final ASTNode value = node == null ? null : XmlChildRole.ATTRIBUTE_VALUE_VALUE_FINDER.findChild(node);
if (value instanceof PsiElement) {
target = (PsiElement) value;
}
}
else if (element instanceof XmlAttributeImpl) {
target = ((XmlAttributeImpl)element).getNameElement();
}
else if (element instanceof XmlTag) {
target = XmlTagUtil.getStartTagNameElement((XmlTag)element);
secondaryTarget = XmlTagUtil.getEndTagNameElement((XmlTag)element);
}
if (host instanceof IdeValidationHost) {
if (target != null) {
((IdeValidationHost)host).addMessageWithFixes(target, message, Validator.ValidationHost.ErrorType.ERROR, intentionActions);
}
if (secondaryTarget != null) {
((IdeValidationHost)host).addMessageWithFixes(secondaryTarget, message, Validator.ValidationHost.ErrorType.ERROR, intentionActions);
}
}
else {
if (target != null) {
host.addMessage(target, message, Validator.ValidationHost.ErrorType.ERROR);
}
if (secondaryTarget != null) {
host.addMessage(secondaryTarget, message, Validator.ValidationHost.ErrorType.ERROR);
}
}
}
public static class RemoveNamespaceDeclarationIntention implements IntentionAction {
private final XmlAttribute myAttribute;
public RemoveNamespaceDeclarationIntention(final @NotNull XmlAttribute attribute) {
myAttribute = attribute;
}
@NotNull
public String getText() {
return FlexBundle.message("remove.namespace.declaration");
}
@NotNull
public String getFamilyName() {
return XmlErrorMessages.message("remove.attribute.quickfix.family");
}
public boolean isAvailable(final @NotNull Project project, final Editor editor, final PsiFile file) {
return myAttribute.isValid();
}
public void invoke(final @NotNull Project project, final Editor editor, final PsiFile file) throws IncorrectOperationException {
final int offset = removeXmlAttribute(myAttribute);
if (offset != -1) {
editor.getCaretModel().moveToOffset(offset);
}
}
/**
* @return offset to move caret to; can be -1
*/
public static int removeXmlAttribute(final XmlAttribute attribute) {
final XmlTag tag = attribute.getParent();
final XmlAttribute nextAttribute = deleteWhiteSpaceTillNextAttribute(attribute);
if (nextAttribute == null) {
deletePreviousWhiteSpaces(attribute);
}
attribute.delete();
XmlUtil.reformatTagStart(tag);
if (nextAttribute != null) {
return nextAttribute.getTextRange().getStartOffset();
}
return -1;
}
@Nullable
private static XmlAttribute deleteWhiteSpaceTillNextAttribute(final XmlAttribute attribute) {
PsiElement nextSibling = attribute.getNextSibling();
while (nextSibling instanceof PsiWhiteSpace) {
final PsiElement whiteSpace = nextSibling;
nextSibling = nextSibling.getNextSibling();
whiteSpace.delete();
}
return nextSibling instanceof XmlAttribute ? (XmlAttribute)nextSibling : null;
}
@Nullable
private static PsiElement deletePreviousWhiteSpaces(final XmlAttribute attribute) {
PsiElement prevSibling = attribute.getPrevSibling();
while (prevSibling instanceof PsiWhiteSpace) {
final PsiElement whiteSpace = prevSibling;
prevSibling = prevSibling.getPrevSibling();
whiteSpace.delete();
}
return prevSibling;
}
public boolean startInWriteAction() {
return true;
}
}
private static class DeclareNamespaceIntention implements IntentionAction {
private final XmlTag myRootTag;
private final String myDefaultPrefix;
private final String myNamespace;
private DeclareNamespaceIntention(final XmlTag rootTag, final String defaultPrefix, final String namespace) {
myRootTag = rootTag;
myDefaultPrefix = defaultPrefix;
myNamespace = namespace;
}
@NotNull
public String getText() {
return FlexBundle.message("declare.namespace", myNamespace);
}
@NotNull
public String getFamilyName() {
return "Declare namespace";
}
public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) {
return myRootTag.isValid();
}
public boolean startInWriteAction() {
return true;
}
public void invoke(final @NotNull Project project, final Editor editor, final PsiFile file) throws IncorrectOperationException {
if (!myRootTag.isValid()) return;
final Set<String> usedPrefixes = myRootTag.getLocalNamespaceDeclarations().keySet();
int postfix = 1;
String nsPrefix = myDefaultPrefix;
while (usedPrefixes.contains(nsPrefix)) {
nsPrefix = myDefaultPrefix + postfix++;
}
XmlNamespaceHelper.getHelper(file).insertNamespaceDeclaration((XmlFile)file, editor, Collections.singleton(myNamespace), nsPrefix, null);
}
}
}