/* * Copyright 2013-2017 consulo.io * * 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 consulo.csharp.lang.doc.parser; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import consulo.csharp.lang.doc.psi.CSharpDocElements; import consulo.csharp.lang.doc.psi.CSharpDocTokenType; import consulo.csharp.lang.doc.validation.CSharpDocAttributeInfo; import consulo.csharp.lang.doc.validation.CSharpDocTagInfo; import consulo.csharp.lang.doc.validation.CSharpDocTagManager; import com.intellij.lang.PsiBuilder; import com.intellij.psi.tree.CustomParsingType; import com.intellij.psi.tree.IElementType; import com.intellij.psi.tree.ILazyParseableElementType; import com.intellij.util.containers.Stack; /** * @author max * @author VISTALL * * Base code from XML plugin, {@see com.intellij.psi.impl.source.parsing.xml.XmlParsing} */ public class CSharpDocParsing { private static final int BALANCING_DEPTH_THRESHOLD = 1000; protected final PsiBuilder myBuilder; private final Stack<String> myTagNamesStack = new Stack<String>(); public CSharpDocParsing(final PsiBuilder builder) { myBuilder = builder; } public void parse() { PsiBuilder.Marker error = null; while(!eof()) { final IElementType tt = token(); if(tt == CSharpDocTokenType.XML_START_TAG_START) { error = flushError(error); parseTag(); } else if(isCommentToken(tt)) { error = flushError(error); parseComment(); } else if(tt == CSharpDocTokenType.XML_REAL_WHITE_SPACE || tt == CSharpDocTokenType.XML_DATA_CHARACTERS || tt == CSharpDocTokenType.DOC_LINE_START) { error = flushError(error); advance(); } else { if(error == null) { error = mark(); } advance(); } } if(error != null) { error.error("Tag is not closed"); } } @Nullable private static PsiBuilder.Marker flushError(PsiBuilder.Marker error) { if(error != null) { error.error("Unexpected tokens"); error = null; } return error; } protected void parseTag() { assert token() == CSharpDocTokenType.XML_START_TAG_START : "Tag start expected"; final PsiBuilder.Marker tag = mark(); final String tagName = parseTagHeader(tag); if(tagName == null) { return; } final PsiBuilder.Marker content = mark(); parseTagContent(); if(token() == CSharpDocTokenType.XML_END_TAG_START) { final PsiBuilder.Marker footer = mark(); advance(); if(token() == CSharpDocTokenType.XML_NAME) { String endName = myBuilder.getTokenText(); if(!tagName.equals(endName) && myTagNamesStack.contains(endName)) { footer.rollbackTo(); myTagNamesStack.pop(); tag.doneBefore(CSharpDocElements.TAG, content, "Element '" + tagName + "' is not closed"); content.drop(); return; } advance(); } footer.drop(); while(token() != CSharpDocTokenType.XML_TAG_END && token() != CSharpDocTokenType.XML_START_TAG_START && token() != CSharpDocTokenType.XML_END_TAG_START && !eof()) { error("Unexpected tokens"); advance(); } if(token() == CSharpDocTokenType.XML_TAG_END) { advance(); } else { error("Closing tag is not done"); } } else { error("Unexpected end of doc"); } content.drop(); myTagNamesStack.pop(); tag.done(CSharpDocElements.TAG); } @Nullable private String parseTagHeader(final PsiBuilder.Marker tag) { advance(); final String tagName; if(token() != CSharpDocTokenType.XML_NAME) { error("Tag name expected"); tagName = ""; } else { tagName = myBuilder.getTokenText(); assert tagName != null; advance(); } myTagNamesStack.push(tagName); do { final IElementType tt = token(); if(tt == CSharpDocTokenType.XML_NAME) { parseAttribute(tagName); } else { break; } } while(true); if(token() == CSharpDocTokenType.XML_EMPTY_ELEMENT_END) { advance(); myTagNamesStack.pop(); tag.done(CSharpDocElements.TAG); return null; } if(token() == CSharpDocTokenType.XML_TAG_END) { advance(); } else { error("Tag is not closed"); myTagNamesStack.pop(); tag.done(CSharpDocElements.TAG); return null; } if(myTagNamesStack.size() > BALANCING_DEPTH_THRESHOLD) { error("Way too unbalanced. Stopping attempt to balance tags properly at this point"); tag.done(CSharpDocElements.TAG); return null; } return tagName; } public void parseTagContent() { PsiBuilder.Marker xmlText = null; while(token() != CSharpDocTokenType.XML_END_TAG_START && !eof()) { final IElementType tt = token(); if(tt == CSharpDocTokenType.XML_START_TAG_START) { xmlText = terminateText(xmlText); parseTag(); } else if(isCommentToken(tt)) { xmlText = terminateText(xmlText); parseComment(); } else if(tt instanceof CustomParsingType || tt instanceof ILazyParseableElementType) { xmlText = terminateText(xmlText); advance(); } else { xmlText = startText(xmlText); advance(); } } terminateText(xmlText); } protected boolean isCommentToken(final IElementType tt) { return tt == CSharpDocTokenType.XML_COMMENT_START; } @NotNull private PsiBuilder.Marker startText(@Nullable PsiBuilder.Marker xmlText) { if(xmlText == null) { xmlText = mark(); assert xmlText != null; } return xmlText; } protected final PsiBuilder.Marker mark() { return myBuilder.mark(); } @Nullable private static PsiBuilder.Marker terminateText(@Nullable PsiBuilder.Marker xmlText) { if(xmlText != null) { xmlText.done(CSharpDocElements.TEXT); xmlText = null; } return xmlText; } protected void parseComment() { advance(); while(true) { final IElementType tt = token(); if(tt == CSharpDocTokenType.XML_COMMENT_CHARACTERS) { advance(); continue; } else if(tt == CSharpDocTokenType.XML_BAD_CHARACTER) { final PsiBuilder.Marker error = mark(); advance(); error.error("Bad character"); continue; } if(tt == CSharpDocTokenType.XML_COMMENT_END) { advance(); } break; } } private void parseAttribute(String tagInfo) { String attributeName = myBuilder.getTokenText(); CSharpDocTagInfo docTagInfo = CSharpDocTagManager.getInstance().getTag(tagInfo); CSharpDocAttributeInfo attributeInfo = docTagInfo == null ? null : docTagInfo.getAttribute(attributeName); assert token() == CSharpDocTokenType.XML_NAME; final PsiBuilder.Marker att = mark(); advance(); if(token() == CSharpDocTokenType.XML_EQ) { advance(); parseAttributeValue(attributeInfo); att.done(CSharpDocElements.ATTRIBUTE); } else { error("'=' expected"); att.done(CSharpDocElements.ATTRIBUTE); } } private void parseAttributeValue(@Nullable CSharpDocAttributeInfo attributeInfo) { final PsiBuilder.Marker attValue = mark(); if(token() == CSharpDocTokenType.XML_ATTRIBUTE_VALUE_START_DELIMITER) { advance(); if(token() == CSharpDocTokenType.XML_BAD_CHARACTER) { final PsiBuilder.Marker error = mark(); advance(); error.error("Unexpected token"); } else if(token() == CSharpDocTokenType.XML_ATTRIBUTE_VALUE_TOKEN) { if(attributeInfo == null) { advance(); } else { switch(attributeInfo.getValueType()) { case REFERENCE: myBuilder.remapCurrentToken(CSharpDocElements.TYPE); break; case PARAMETER: myBuilder.remapCurrentToken(CSharpDocElements.PARAMETER_EXPRESSION); break; case TYPE_PARAMETER: myBuilder.remapCurrentToken(CSharpDocElements.GENERIC_PARAMETER_EXPRESSION); break; } advance(); } } if(token() == CSharpDocTokenType.XML_ATTRIBUTE_VALUE_END_DELIMITER) { advance(); } else { error("Attribute value is not closed"); } } else { error("Attribute value expected"); } attValue.done(CSharpDocElements.ATTRIBUTE_VALUE); } @Nullable protected final IElementType token() { return myBuilder.getTokenType(); } protected final boolean eof() { return myBuilder.eof(); } protected final void advance() { myBuilder.advanceLexer(); } private void error(final String message) { myBuilder.error(message); } }