| /* |
| * Copyright 2000-2014 JetBrains s.r.o. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.intellij.psi.formatter.xml; |
| |
| import com.intellij.formatting.*; |
| import com.intellij.lang.*; |
| import com.intellij.lang.xml.XMLLanguage; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.fileTypes.StdFileTypes; |
| import com.intellij.openapi.util.TextRange; |
| import com.intellij.psi.*; |
| import com.intellij.psi.codeStyle.CommonCodeStyleSettings; |
| import com.intellij.psi.formatter.WhiteSpaceFormattingStrategy; |
| import com.intellij.psi.formatter.WhiteSpaceFormattingStrategyFactory; |
| import com.intellij.psi.formatter.common.AbstractBlock; |
| import com.intellij.psi.impl.source.SourceTreeToPsiMap; |
| import com.intellij.psi.impl.source.tree.LeafElement; |
| import com.intellij.psi.impl.source.tree.TreeElement; |
| import com.intellij.psi.impl.source.tree.TreeUtil; |
| import com.intellij.psi.search.PsiElementProcessor; |
| import com.intellij.psi.templateLanguages.TemplateLanguageFileViewProvider; |
| import com.intellij.psi.tree.IElementType; |
| import com.intellij.psi.tree.TokenSet; |
| import com.intellij.psi.xml.*; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| |
| public abstract class AbstractXmlBlock extends AbstractBlock { |
| protected XmlFormattingPolicy myXmlFormattingPolicy; |
| protected final XmlInjectedLanguageBlockBuilder myInjectedBlockBuilder; |
| private final boolean myPreserveSpace; |
| |
| protected AbstractXmlBlock(final ASTNode node, |
| final Wrap wrap, |
| final Alignment alignment, |
| final XmlFormattingPolicy policy) { |
| this(node, wrap, alignment, policy, false); |
| } |
| |
| |
| protected AbstractXmlBlock(final ASTNode node, |
| final Wrap wrap, |
| final Alignment alignment, |
| final XmlFormattingPolicy policy, |
| final boolean preserveSpace) { |
| super(node, wrap, alignment); |
| myXmlFormattingPolicy = policy; |
| if (node.getTreeParent() == null) { |
| myXmlFormattingPolicy.setRootBlock(node, this); |
| } |
| myInjectedBlockBuilder = new XmlInjectedLanguageBlockBuilder(myXmlFormattingPolicy); |
| myPreserveSpace = shouldPreserveSpace(node, preserveSpace); |
| } |
| |
| |
| /** |
| * Handles xml:space='preserve|default' attribute. |
| * See <a href="http://www.w3.org/TR/2004/REC-xml-20040204/#sec-white-space">Extensible Markup Language (XML) 1.0 (Third Edition), |
| * White Space Handling</a> |
| * |
| * @return True if the space must be preserved (xml:space='preserve'), false if the attribute |
| * contains 'default'. If the attribute is not defined, return the current value. |
| */ |
| private static boolean shouldPreserveSpace(ASTNode node, boolean defaultValue) { |
| if (node.getPsi() instanceof XmlTag) { |
| XmlTag tag = (XmlTag)node.getPsi(); |
| if (tag != null) { |
| XmlAttribute spaceAttr = tag.getAttribute("xml:space"); |
| if (spaceAttr != null) { |
| String value = spaceAttr.getValue(); |
| if ("preserve".equals(value)) { |
| return true; |
| } |
| if ("default".equals(value)) { |
| return false; |
| } |
| } |
| } |
| } |
| return defaultValue; |
| } |
| |
| public boolean isPreserveSpace() { |
| return myPreserveSpace; |
| } |
| |
| |
| public static WrapType getWrapType(final int type) { |
| if (type == CommonCodeStyleSettings.DO_NOT_WRAP) return WrapType.NONE; |
| if (type == CommonCodeStyleSettings.WRAP_ALWAYS) return WrapType.ALWAYS; |
| if (type == CommonCodeStyleSettings.WRAP_AS_NEEDED) return WrapType.NORMAL; |
| return WrapType.CHOP_DOWN_IF_LONG; |
| } |
| |
| protected Alignment chooseAlignment(final ASTNode child, final Alignment attrAlignment, final Alignment textAlignment) { |
| if (myNode.getElementType() == XmlElementType.XML_TEXT) return getAlignment(); |
| final IElementType elementType = child.getElementType(); |
| if (elementType == XmlElementType.XML_ATTRIBUTE && myXmlFormattingPolicy.getShouldAlignAttributes()) return attrAlignment; |
| if (elementType == XmlElementType.XML_TEXT && myXmlFormattingPolicy.getShouldAlignText()) return textAlignment; |
| return null; |
| } |
| |
| private Wrap getTagEndWrapping(final XmlTag parent) { |
| return Wrap.createWrap(myXmlFormattingPolicy.getWrappingTypeForTagEnd(parent), true); |
| } |
| |
| protected Wrap chooseWrap(final ASTNode child, final Wrap tagBeginWrap, final Wrap attrWrap, final Wrap textWrap) { |
| if (myNode.getElementType() == XmlElementType.XML_TEXT) return textWrap; |
| final IElementType elementType = child.getElementType(); |
| if (elementType == XmlElementType.XML_ATTRIBUTE) return attrWrap; |
| if (elementType == XmlTokenType.XML_START_TAG_START) return tagBeginWrap; |
| if (elementType == XmlTokenType.XML_END_TAG_START) { |
| final PsiElement parent = SourceTreeToPsiMap.treeElementToPsi(child.getTreeParent()); |
| if (parent instanceof XmlTag) { |
| final XmlTag tag = (XmlTag)parent; |
| if (canWrapTagEnd(tag)) { |
| return getTagEndWrapping(tag); |
| } |
| } |
| return null; |
| } |
| if (elementType == XmlElementType.XML_TEXT || elementType == XmlTokenType.XML_DATA_CHARACTERS) return textWrap; |
| return null; |
| } |
| |
| protected boolean canWrapTagEnd(final XmlTag tag) { |
| return tag.getSubTags().length > 0; |
| } |
| |
| protected XmlTag getTag() { |
| return getTag(myNode); |
| } |
| |
| protected static XmlTag getTag(final ASTNode node) { |
| final PsiElement element = SourceTreeToPsiMap.treeElementToPsi(node); |
| if (element instanceof XmlTag) { |
| return (XmlTag)element; |
| } |
| else { |
| return null; |
| } |
| } |
| |
| protected Wrap createTagBeginWrapping(final XmlTag tag) { |
| return Wrap.createWrap(myXmlFormattingPolicy.getWrappingTypeForTagBegin(tag), true); |
| } |
| |
| @Nullable |
| protected |
| ASTNode processChild(List<Block> result, |
| final ASTNode child, |
| final Wrap wrap, |
| final Alignment alignment, |
| final Indent indent) { |
| final Language myLanguage = myNode.getPsi().getLanguage(); |
| final PsiElement childPsi = child.getPsi(); |
| final Language childLanguage = childPsi.getLanguage(); |
| if (useMyFormatter(myLanguage, childLanguage, childPsi)) { |
| |
| XmlTag tag = getAnotherTreeTag(child); |
| if (tag != null |
| && containsTag(tag) |
| && doesNotIntersectSubTagsWith(tag)) { |
| ASTNode currentChild = createAnotherTreeNode(result, child, tag, indent, wrap, alignment); |
| |
| if (currentChild == null) { |
| return null; |
| } |
| |
| while (currentChild != null && currentChild.getTreeParent() != myNode && currentChild.getTreeParent() != child.getTreeParent()) { |
| currentChild = processAllChildrenFrom(result, currentChild, wrap, alignment, indent); |
| if (currentChild != null && (currentChild.getTreeParent() == myNode || currentChild.getTreeParent() == child.getTreeParent())) { |
| return currentChild; |
| } |
| if (currentChild != null) { |
| currentChild = currentChild.getTreeParent(); |
| |
| } |
| } |
| |
| return currentChild; |
| } |
| |
| processSimpleChild(child, indent, result, wrap, alignment); |
| return child; |
| } |
| else { |
| myInjectedBlockBuilder.addInjectedLanguageBlockWrapper(result, child, indent, 0, null); |
| return child; |
| } |
| } |
| |
| protected boolean doesNotIntersectSubTagsWith(final PsiElement tag) { |
| final TextRange tagRange = tag.getTextRange(); |
| final XmlTag[] subTags = getSubTags(); |
| for (XmlTag subTag : subTags) { |
| final TextRange subTagRange = subTag.getTextRange(); |
| if (subTagRange.getEndOffset() < tagRange.getStartOffset()) continue; |
| if (subTagRange.getStartOffset() > tagRange.getEndOffset()) return true; |
| |
| if (tagRange.getStartOffset() > subTagRange.getStartOffset() && tagRange.getEndOffset() < subTagRange.getEndOffset()) return false; |
| if (tagRange.getEndOffset() > subTagRange.getStartOffset() && tagRange.getEndOffset() < subTagRange.getEndOffset()) return false; |
| |
| } |
| return true; |
| } |
| |
| private XmlTag[] getSubTags() { |
| |
| if (myNode instanceof XmlTag) { |
| return ((XmlTag)myNode.getPsi()).getSubTags(); |
| } |
| else if (myNode.getPsi() instanceof XmlElement) { |
| return collectSubTags((XmlElement)myNode.getPsi()); |
| } |
| else { |
| return new XmlTag[0]; |
| } |
| |
| } |
| |
| private static XmlTag[] collectSubTags(final XmlElement node) { |
| final List<XmlTag> result = new ArrayList<XmlTag>(); |
| node.processElements(new PsiElementProcessor() { |
| @Override |
| public boolean execute(@NotNull final PsiElement element) { |
| if (element instanceof XmlTag) { |
| result.add((XmlTag)element); |
| } |
| return true; |
| } |
| }, node); |
| return result.toArray(new XmlTag[result.size()]); |
| } |
| |
| protected boolean containsTag(final PsiElement tag) { |
| final ASTNode closingTagStart = XmlChildRole.CLOSING_TAG_START_FINDER.findChild(myNode); |
| final ASTNode startTagStart = XmlChildRole.START_TAG_END_FINDER.findChild(myNode); |
| |
| if (closingTagStart == null && startTagStart == null) { |
| return tag.getTextRange().getEndOffset() <= myNode.getTextRange().getEndOffset(); |
| } |
| else if (closingTagStart == null) { |
| return false; |
| } |
| else { |
| return tag.getTextRange().getEndOffset() <= closingTagStart.getTextRange().getEndOffset(); |
| } |
| } |
| |
| private ASTNode processAllChildrenFrom(final List<Block> result, |
| @NotNull final ASTNode child, |
| final Wrap wrap, |
| final Alignment alignment, |
| final Indent indent) { |
| ASTNode resultNode = child; |
| ASTNode currentChild = child.getTreeNext(); |
| while (currentChild != null && currentChild.getElementType() != XmlTokenType.XML_END_TAG_START) { |
| if (!containsWhiteSpacesOnly(currentChild)) { |
| currentChild = processChild(result, currentChild, wrap, alignment, indent); |
| resultNode = currentChild; |
| } |
| if (currentChild != null) { |
| currentChild = currentChild.getTreeNext(); |
| } |
| } |
| return resultNode; |
| } |
| |
| protected void processSimpleChild(final ASTNode child, |
| final Indent indent, |
| final List<Block> result, |
| final Wrap wrap, |
| final Alignment alignment) { |
| if (isXmlTag(child)) { |
| result.add(createTagBlock(child, indent != null ? indent : Indent.getNoneIndent(), wrap, alignment)); |
| } else if (child.getElementType() == XmlElementType.XML_DOCTYPE) { |
| result.add( |
| new XmlBlock(child, wrap, alignment, myXmlFormattingPolicy, indent, null, isPreserveSpace()) { |
| @Override |
| protected Wrap getDefaultWrap(final ASTNode node) { |
| final IElementType type = node.getElementType(); |
| return type == XmlTokenType.XML_ATTRIBUTE_VALUE_TOKEN |
| ? Wrap.createWrap(getWrapType(myXmlFormattingPolicy.getAttributesWrap()), false) : null; |
| } |
| } |
| ); |
| } |
| else { |
| result.add(createSimpleChild(child, indent, wrap, alignment)); |
| } |
| } |
| |
| |
| protected XmlBlock createSimpleChild(final ASTNode child, final Indent indent, final Wrap wrap, final Alignment alignment) { |
| return new XmlBlock(child, wrap, alignment, myXmlFormattingPolicy, indent, null, isPreserveSpace()); |
| } |
| |
| protected XmlTagBlock createTagBlock(final ASTNode child, final Indent indent, final Wrap wrap, final Alignment alignment) { |
| return new XmlTagBlock(child, wrap, alignment, myXmlFormattingPolicy, indent != null ? indent : Indent.getNoneIndent(), isPreserveSpace()); |
| } |
| |
| @Nullable |
| protected XmlTag findXmlTagAt(final ASTNode child, final int startOffset) { |
| return null; |
| } |
| |
| @Nullable |
| protected ASTNode createAnotherTreeNode(final List<Block> result, |
| final ASTNode child, |
| PsiElement tag, |
| final Indent indent, |
| final Wrap wrap, final Alignment alignment) { |
| return null; |
| } |
| |
| @Nullable |
| protected Block createAnotherTreeTagBlock(final PsiElement tag, final Indent childIndent) { |
| return null; |
| } |
| |
| protected XmlFormattingPolicy createPolicyFor() { |
| return myXmlFormattingPolicy; |
| } |
| |
| @Nullable |
| protected XmlTag getAnotherTreeTag(final ASTNode child) { |
| return null; |
| |
| } |
| protected boolean isXmlTag(final ASTNode child) { |
| return isXmlTag(child.getPsi()); |
| } |
| |
| protected boolean isXmlTag(final PsiElement psi) { |
| return psi instanceof XmlTag; |
| } |
| |
| protected boolean useMyFormatter(final Language myLanguage, final Language childLanguage, final PsiElement childPsi) { |
| if (myLanguage == childLanguage || |
| childLanguage == StdFileTypes.HTML.getLanguage() || |
| childLanguage == StdFileTypes.XHTML.getLanguage() || |
| childLanguage == StdFileTypes.XML.getLanguage()) { |
| return true; |
| } |
| final FormattingModelBuilder childFormatter = LanguageFormatting.INSTANCE.forLanguage(childLanguage); |
| return childFormatter == null || |
| childFormatter instanceof DelegatingFormattingModelBuilder && |
| ((DelegatingFormattingModelBuilder)childFormatter).dontFormatMyModel(); |
| } |
| |
| protected boolean isJspxJavaContainingNode(final ASTNode child) { |
| return false; |
| } |
| |
| public abstract boolean insertLineBreakBeforeTag(); |
| |
| public abstract boolean removeLineBreakBeforeTag(); |
| |
| protected Spacing createDefaultSpace(boolean forceKeepLineBreaks, final boolean inText) { |
| boolean shouldKeepLineBreaks = getShouldKeepLineBreaks(inText, forceKeepLineBreaks); |
| return Spacing.createSpacing(0, Integer.MAX_VALUE, 0, shouldKeepLineBreaks, myXmlFormattingPolicy.getKeepBlankLines()); |
| } |
| |
| private boolean getShouldKeepLineBreaks(final boolean inText, final boolean forceKeepLineBreaks) { |
| if (forceKeepLineBreaks) { |
| return true; |
| } |
| if (inText && myXmlFormattingPolicy.getShouldKeepLineBreaksInText()) { |
| return true; |
| } |
| if (!inText && myXmlFormattingPolicy.getShouldKeepLineBreaks()) { |
| return true; |
| } |
| return false; |
| } |
| |
| public abstract boolean isTextElement(); |
| |
| private static final Logger LOG = Logger.getInstance("#com.intellij.psi.formatter.xml.AbstractXmlBlock"); |
| |
| protected void createJspTextNode(final List<Block> localResult, final ASTNode child, final Indent indent) { |
| } |
| |
| @Nullable |
| protected static ASTNode findChildAfter(@NotNull final ASTNode child, final int endOffset) { |
| TreeElement fileNode = TreeUtil.getFileElement((TreeElement)child); |
| final LeafElement leaf = fileNode.findLeafElementAt(endOffset); |
| if (leaf != null && leaf.getStartOffset() == endOffset && endOffset > 0) { |
| return fileNode.findLeafElementAt(endOffset - 1); |
| } |
| return leaf; |
| } |
| |
| @Override |
| public boolean isLeaf() { |
| return (isComment(myNode)) || |
| myNode.getElementType() == TokenType.WHITE_SPACE || |
| myNode.getElementType() == XmlTokenType.XML_DATA_CHARACTERS || |
| myNode.getElementType() == XmlTokenType.XML_ATTRIBUTE_VALUE_TOKEN; |
| } |
| |
| private static boolean isComment(final ASTNode node) { |
| final PsiElement psiElement = SourceTreeToPsiMap.treeElementToPsi(node); |
| if (psiElement instanceof PsiComment) return true; |
| final ParserDefinition parserDefinition = LanguageParserDefinitions.INSTANCE.forLanguage(psiElement.getLanguage()); |
| if (parserDefinition == null) return false; |
| final TokenSet commentTokens = parserDefinition.getCommentTokens(); |
| return commentTokens.contains(node.getElementType()); |
| } |
| |
| public void setXmlFormattingPolicy(final XmlFormattingPolicy xmlFormattingPolicy) { |
| myXmlFormattingPolicy = xmlFormattingPolicy; |
| } |
| |
| protected boolean buildInjectedPsiBlocks(List<Block> result, final ASTNode child, Wrap wrap, Alignment alignment, Indent indent) { |
| if (myInjectedBlockBuilder.addInjectedBlocks(result, child, wrap, alignment, indent)) { |
| return true; |
| } |
| |
| PsiFile containingFile = child.getPsi().getContainingFile(); |
| FileViewProvider fileViewProvider = containingFile.getViewProvider(); |
| |
| if (fileViewProvider instanceof TemplateLanguageFileViewProvider) { |
| Language templateLanguage = ((TemplateLanguageFileViewProvider)fileViewProvider).getTemplateDataLanguage(); |
| PsiElement at = fileViewProvider.findElementAt(child.getStartOffset(), templateLanguage); |
| |
| if (at instanceof XmlToken) { |
| at = at.getParent(); |
| } |
| |
| // TODO: several comments |
| if (at instanceof PsiComment && |
| at.getTextRange().equals(child.getTextRange()) && |
| at.getNode() != child) { |
| return buildInjectedPsiBlocks(result, at.getNode(), wrap, alignment, indent); |
| } |
| } |
| |
| return false; |
| } |
| |
| public boolean isCDATAStart() { |
| return myNode.getElementType() == XmlTokenType.XML_CDATA_START; |
| } |
| |
| public boolean isCDATAEnd() { |
| return myNode.getElementType() == XmlTokenType.XML_CDATA_END; |
| } |
| |
| public static boolean containsWhiteSpacesOnly(@NotNull ASTNode node) { |
| PsiElement psiElement = node.getPsi(); |
| if (psiElement instanceof PsiWhiteSpace) return true; |
| Language nodeLang = psiElement.getLanguage(); |
| if (!nodeLang.isKindOf(XMLLanguage.INSTANCE) || |
| isTextOnlyNode(node) || |
| node.getElementType() == XmlElementType.XML_PROLOG) { |
| WhiteSpaceFormattingStrategy strategy = WhiteSpaceFormattingStrategyFactory.getStrategy(nodeLang); |
| int length = node.getTextLength(); |
| return strategy.check(node.getChars(), 0, length) >= length; |
| } |
| return false; |
| } |
| |
| private static boolean isTextOnlyNode(@NotNull ASTNode node) { |
| if (node.getPsi() instanceof XmlText) return true; |
| ASTNode firstChild = node.getFirstChildNode(); |
| ASTNode lastChild = node.getLastChildNode(); |
| if (firstChild != null && firstChild == lastChild && firstChild.getPsi() instanceof XmlText) { |
| return true; |
| } |
| return false; |
| } |
| |
| } |