mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
IJPL-163136 introduce syntax XML parser
GitOrigin-RevId: 2b3cd5542d092d3eec1e6646f6003707f6ccdfd5
This commit is contained in:
committed by
intellij-monorepo-bot
parent
77b888bda1
commit
05fa043467
@@ -6,10 +6,14 @@ import com.intellij.lang.LanguageASTFactory;
|
||||
import com.intellij.lang.xml.XMLLanguage;
|
||||
import com.intellij.lang.xml.XMLParserDefinition;
|
||||
import com.intellij.lang.xml.XmlASTFactory;
|
||||
import com.intellij.lang.xml.XmlSyntaxDefinitionExtension;
|
||||
import com.intellij.openapi.application.PluginPathManager;
|
||||
import com.intellij.openapi.extensions.ExtensionPointName;
|
||||
import com.intellij.openapi.util.SystemInfo;
|
||||
import com.intellij.platform.syntax.psi.ElementTypeConverters;
|
||||
import com.intellij.platform.syntax.psi.LanguageSyntaxDefinitions;
|
||||
import com.intellij.psi.xml.StartTagEndTokenProvider;
|
||||
import com.intellij.psi.xml.XmlElementTypeConverterExtension;
|
||||
import com.intellij.testFramework.ParsingTestCase;
|
||||
|
||||
import static com.intellij.xml.testFramework.XmlElementTypeServiceHelper.registerXmlElementTypeServices;
|
||||
@@ -25,6 +29,8 @@ public class AntParsingTest extends ParsingTestCase {
|
||||
super.setUp();
|
||||
registerXmlElementTypeServices(getApplication(), getTestRootDisposable());
|
||||
addExplicitExtension(LanguageASTFactory.INSTANCE, XMLLanguage.INSTANCE, new XmlASTFactory());
|
||||
addExplicitExtension(LanguageSyntaxDefinitions.getINSTANCE(), XMLLanguage.INSTANCE, new XmlSyntaxDefinitionExtension());
|
||||
addExplicitExtension(ElementTypeConverters.getInstance(), XMLLanguage.INSTANCE, new XmlElementTypeConverterExtension());
|
||||
registerExtensionPoint(new ExtensionPointName<>("com.intellij.xml.startTagEndToken"),
|
||||
StartTagEndTokenProvider.class);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ jvm_library(
|
||||
"//xml/xml-parser:parser",
|
||||
"//platform/core-impl",
|
||||
"//tools/intellij.tools.ide.metrics.benchmark:ide-metrics-benchmark",
|
||||
"//platform/syntax/syntax-psi:psi",
|
||||
]
|
||||
)
|
||||
### auto-generated section `build intellij.xml.testFramework` end
|
||||
@@ -14,6 +14,7 @@
|
||||
<orderEntry type="module" module-name="intellij.xml.parser" />
|
||||
<orderEntry type="module" module-name="intellij.platform.core.impl" />
|
||||
<orderEntry type="module" module-name="intellij.tools.ide.metrics.benchmark" />
|
||||
<orderEntry type="module" module-name="intellij.platform.syntax.psi" />
|
||||
</component>
|
||||
<component name="copyright">
|
||||
<Base>
|
||||
|
||||
@@ -6,8 +6,11 @@ import com.intellij.lang.html.HTMLLanguage;
|
||||
import com.intellij.lang.html.HTMLParserDefinition;
|
||||
import com.intellij.lang.xml.XMLLanguage;
|
||||
import com.intellij.lang.xml.XmlASTFactory;
|
||||
import com.intellij.lang.xml.XmlSyntaxDefinitionExtension;
|
||||
import com.intellij.lexer.EmbeddedTokenTypesProvider;
|
||||
import com.intellij.lexer.Lexer;
|
||||
import com.intellij.platform.syntax.psi.ElementTypeConverters;
|
||||
import com.intellij.platform.syntax.psi.LanguageSyntaxDefinitions;
|
||||
import com.intellij.psi.PsiFile;
|
||||
import com.intellij.psi.PsiManager;
|
||||
import com.intellij.psi.impl.source.tree.LeafElement;
|
||||
@@ -50,6 +53,8 @@ public abstract class XmlParsingTestBase extends ParsingTestCase {
|
||||
registerXmlElementTypeServices(getApplication(), getTestRootDisposable());
|
||||
addExplicitExtension(LanguageASTFactory.INSTANCE, XMLLanguage.INSTANCE, new XmlASTFactory());
|
||||
registerExtensionPoint(StartTagEndTokenProvider.EP_NAME, StartTagEndTokenProvider.class);
|
||||
addExplicitExtension(LanguageSyntaxDefinitions.getINSTANCE(), XMLLanguage.INSTANCE, new XmlSyntaxDefinitionExtension());
|
||||
addExplicitExtension(ElementTypeConverters.getInstance(), XMLLanguage.INSTANCE, new XmlElementTypeConverterExtension());
|
||||
}
|
||||
|
||||
protected void doTestXml(@NonNls String text) throws Exception {
|
||||
|
||||
@@ -46,6 +46,8 @@
|
||||
<orderEntry type="module" module-name="intellij.tools.ide.metrics.benchmark" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.platform.polySymbols.testFramework" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.libraries.xerces" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.platform.syntax.scripts" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.xml.syntax" scope="TEST" />
|
||||
</component>
|
||||
<component name="copyright">
|
||||
<Base>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.syntax
|
||||
|
||||
import com.intellij.platform.syntax.scripts.assertPropertiesMatch
|
||||
import com.intellij.xml.syntax.DefaultXmlSyntaxResourcesTestAccessor
|
||||
import com.intellij.xml.syntax.XmlSyntaxBundle
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class XmlSyntaxDefaultResourceTest {
|
||||
@Test
|
||||
fun testResourcesMatch() {
|
||||
assertPropertiesMatch(
|
||||
propertiesFileName = XmlSyntaxBundle.BUNDLE,
|
||||
defaultResourcesFileName = DefaultXmlSyntaxResourcesTestAccessor.defaultJavaSyntaxResourcesName,
|
||||
classLoader = XmlSyntaxBundle.javaClass.classLoader,
|
||||
actualMapping = DefaultXmlSyntaxResourcesTestAccessor.mappings,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,12 @@ import com.intellij.psi.xml.XmlTokenType
|
||||
import com.intellij.util.ThreeState
|
||||
import com.intellij.util.TripleFunction
|
||||
import com.intellij.util.diff.FlyweightCapableTreeStructure
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
|
||||
/**
|
||||
* Use [com.intellij.xml.syntax.XmlParser] instead
|
||||
*/
|
||||
@ApiStatus.Obsolete
|
||||
open class XmlParser : PsiParser, LightPsiParser {
|
||||
override fun parse(
|
||||
root: IElementType,
|
||||
@@ -36,7 +41,7 @@ open class XmlParser : PsiParser, LightPsiParser {
|
||||
}
|
||||
|
||||
// tries to match an old and new XmlTag by name
|
||||
private val REPARSE_XML_TAG_BY_NAME: TripleFunction<ASTNode, LighterASTNode, FlyweightCapableTreeStructure<LighterASTNode>, ThreeState> =
|
||||
internal val REPARSE_XML_TAG_BY_NAME: TripleFunction<ASTNode, LighterASTNode, FlyweightCapableTreeStructure<LighterASTNode>, ThreeState> =
|
||||
TripleFunction(::reparseXmlTagByName)
|
||||
|
||||
private fun reparseXmlTagByName(
|
||||
|
||||
@@ -46,10 +46,14 @@ import com.intellij.psi.xml.XmlTokenType.XML_TAG_CHARACTERS
|
||||
import com.intellij.psi.xml.XmlTokenType.XML_TAG_END
|
||||
import com.intellij.util.containers.Stack
|
||||
import com.intellij.xml.parsing.XmlParserBundle.message
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
|
||||
/*
|
||||
/**
|
||||
* Use [com.intellij.xml.syntax.XmlParsing] instead.
|
||||
*
|
||||
* @author max
|
||||
*/
|
||||
@ApiStatus.Obsolete
|
||||
open class XmlParsing(
|
||||
@JvmField
|
||||
protected val myBuilder: PsiBuilder,
|
||||
|
||||
@@ -117,7 +117,7 @@ object XmlElementType {
|
||||
|
||||
@JvmField
|
||||
val XML_FILE: IFileElementType =
|
||||
IFileElementType(XMLLanguage.INSTANCE)
|
||||
XmlFileElementType()
|
||||
|
||||
@JvmField
|
||||
val XHTML_FILE: IElementType =
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.psi.xml
|
||||
|
||||
import com.intellij.lang.ASTNode
|
||||
import com.intellij.lang.xml.XMLLanguage
|
||||
import com.intellij.platform.syntax.emptySyntaxElementTypeSet
|
||||
import com.intellij.platform.syntax.psi.PsiSyntaxBuilderFactory
|
||||
import com.intellij.platform.syntax.psi.registerParse
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.psi.impl.source.parsing.xml.REPARSE_XML_TAG_BY_NAME
|
||||
import com.intellij.psi.tree.IFileElementType
|
||||
import com.intellij.xml.syntax.XmlParser
|
||||
|
||||
class XmlFileElementType : IFileElementType(XMLLanguage.INSTANCE) {
|
||||
override fun doParseContents(chameleon: ASTNode, psi: PsiElement): ASTNode {
|
||||
val factory = PsiSyntaxBuilderFactory.getInstance()
|
||||
val builder = factory.createBuilder(
|
||||
chameleon = chameleon,
|
||||
lang = language,
|
||||
text = chameleon.chars,
|
||||
)
|
||||
val syntaxTreeBuilder = builder.getSyntaxTreeBuilder()
|
||||
|
||||
val startTime = System.nanoTime()
|
||||
syntaxTreeBuilder.enforceCommentTokens(emptySyntaxElementTypeSet())
|
||||
builder.setCustomComparator(REPARSE_XML_TAG_BY_NAME)
|
||||
XmlParser().parse(syntaxTreeBuilder)
|
||||
val node = builder.getTreeBuilt()
|
||||
registerParse(builder, XMLLanguage.INSTANCE, System.nanoTime() - startTime)
|
||||
return node.getFirstChildNode()
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,8 @@ jvm_library(
|
||||
"//platform/markdown-utils",
|
||||
"//platform/polySymbols",
|
||||
"//RegExpSupport:regexp",
|
||||
"//platform/syntax/syntax-psi:psi",
|
||||
"//xml/xml-syntax:syntax",
|
||||
],
|
||||
runtime_deps = [
|
||||
":psi-impl_resources",
|
||||
|
||||
@@ -34,5 +34,7 @@
|
||||
<orderEntry type="module" module-name="intellij.platform.markdown.utils" />
|
||||
<orderEntry type="module" module-name="intellij.platform.polySymbols" />
|
||||
<orderEntry type="module" module-name="intellij.regexp" />
|
||||
<orderEntry type="module" module-name="intellij.platform.syntax.psi" />
|
||||
<orderEntry type="module" module-name="intellij.xml.syntax" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -3,6 +3,7 @@
|
||||
<module name="intellij.xml.psi"/>
|
||||
<module name="intellij.xml.parser"/>
|
||||
<module name="intellij.regexp"/>
|
||||
<module name="intellij.xml.syntax"/>
|
||||
</dependencies>
|
||||
|
||||
<projectListeners>
|
||||
@@ -31,6 +32,8 @@
|
||||
<lang.parserDefinition language="HTML" implementationClass="com.intellij.lang.html.HTMLParserDefinition"/>
|
||||
<lang.parserDefinition language="XHTML" implementationClass="com.intellij.lang.xhtml.XHTMLParserDefinition"/>
|
||||
|
||||
<syntax.syntaxDefinition language="XML" implementationClass="com.intellij.lang.xml.XmlSyntaxDefinitionExtension"/>
|
||||
|
||||
<applicationService serviceInterface="com.intellij.html.embedding.BasicHtmlRawTextElementFactory"
|
||||
serviceImplementation="com.intellij.lang.html.HtmlRawTextElementFactoryImpl"/>
|
||||
<applicationService serviceImplementation="com.intellij.application.options.editor.XmlFoldingSettings"/>
|
||||
|
||||
@@ -7,6 +7,7 @@ package com.intellij.lang.xml;
|
||||
|
||||
import com.intellij.lang.ASTFactory;
|
||||
import com.intellij.lang.impl.PsiBuilderImpl;
|
||||
import com.intellij.platform.syntax.psi.ExtraWhitespaces;
|
||||
import com.intellij.psi.impl.source.html.HtmlDocumentImpl;
|
||||
import com.intellij.psi.impl.source.html.HtmlTagImpl;
|
||||
import com.intellij.psi.impl.source.tree.*;
|
||||
@@ -136,6 +137,17 @@ public class XmlASTFactory extends ASTFactory {
|
||||
}
|
||||
|
||||
static {
|
||||
/*
|
||||
XML has a special whitespace kind `XmlTokenType.XML_REAL_WHITE_SPACE`.
|
||||
On AST construction, a leaf with this token type gets replaced with a plain PsiWhiteSpaceImpl
|
||||
(which has the plain WHITE_SPACE token type), see `XmlASTFactory.createLeaf`, so XML_REAL_WHITE_SPACE exists during parsing only.
|
||||
|
||||
But when we want to reparse the file, we end up in a situation when a new not-yet-built tree is
|
||||
compared with the existing tree. And here, we need to deal with inconsistency in token types between XML_REAL_WHITE_SPACE in the new tree and WHITE_SPACE in the old tree.
|
||||
|
||||
To overcome this situation, this hack is introduced
|
||||
*/
|
||||
PsiBuilderImpl.registerWhitespaceToken(XML_REAL_WHITE_SPACE);
|
||||
ExtraWhitespaces.registerExtraWhitespace(XML_REAL_WHITE_SPACE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.lang.xml
|
||||
|
||||
import com.intellij.platform.syntax.LanguageSyntaxDefinition
|
||||
import com.intellij.platform.syntax.SyntaxElementTypeSet
|
||||
import com.intellij.platform.syntax.lexer.Lexer
|
||||
import com.intellij.platform.syntax.parser.SyntaxTreeBuilder
|
||||
import com.intellij.xml.syntax.XmlParser
|
||||
import com.intellij.xml.syntax.XmlSyntaxDefinition
|
||||
|
||||
class XmlSyntaxDefinitionExtension : LanguageSyntaxDefinition {
|
||||
fun getWhitespaceTokens(): SyntaxElementTypeSet = XmlSyntaxDefinition.WHITESPACES
|
||||
|
||||
override fun parse(builder: SyntaxTreeBuilder) {
|
||||
XmlParser().parse(builder)
|
||||
}
|
||||
|
||||
override fun createLexer(): Lexer = XmlSyntaxDefinition.createLexer()
|
||||
|
||||
override val comments: SyntaxElementTypeSet
|
||||
get() = XmlSyntaxDefinition.COMMENTS
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.intellij.xml.syntax
|
||||
|
||||
// Generated by generateBundleMapping.kt
|
||||
// To regenerate the file, please run `GenerateBundleMapping` run configuration with the following parameters:
|
||||
// <full path to XmlSyntaxBundle.properties> <full path to DefaultXmlSyntaxResources.kt> com.intellij.xml.syntax
|
||||
internal object DefaultXmlSyntaxResources {
|
||||
val mappings: Map<String, String> = mapOf(
|
||||
"xml.parsing.absent.root.tag" to "Valid XML document must have a root tag",
|
||||
"xml.parsing.attribute.value.expected" to "Attribute value expected",
|
||||
"xml.parsing.bad.character" to "Bad character",
|
||||
"xml.parsing.closing.tag.is.not.done" to "Closing tag is not valid",
|
||||
"xml.parsing.closing.tag.matches.nothing" to "Closing tag matches nothing",
|
||||
"xml.parsing.closing.tag.name.missing" to "Closing tag name is missing",
|
||||
"xml.parsing.expected.attribute.eq.sign" to "= expected",
|
||||
"xml.parsing.multiple.root.tags" to "Multiple root tags",
|
||||
"xml.parsing.named.element.is.not.closed" to "Element {0} is not closed",
|
||||
"xml.parsing.processing.instruction.name.expected" to "Processing instruction name expected",
|
||||
"xml.parsing.tag.name.expected" to "Tag name expected",
|
||||
"xml.parsing.tag.start.is.not.closed" to "Tag start is not closed",
|
||||
"xml.parsing.top.level.element.is.not.completed" to "Top level element is not completed",
|
||||
"xml.parsing.unclosed.attribute.value" to "Attribute value is not closed",
|
||||
"xml.parsing.unescaped.ampersand.or.nonterminated.character.entity.reference" to "Unescaped \\& or nonterminated character/entity reference",
|
||||
"xml.parsing.unexpected.end.of.file" to "Unexpected end of file",
|
||||
"xml.parsing.unexpected.token" to "Unexpected token",
|
||||
"xml.parsing.unexpected.tokens" to "Unexpected tokens",
|
||||
"xml.parsing.unterminated.processing.instruction" to "Processing instruction not terminated",
|
||||
"xml.parsing.way.too.unbalanced" to "Way too unbalanced. Stopping attempt to balance tags properly at this point",
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
xml.parsing.absent.root.tag=Valid XML document must have a root tag
|
||||
xml.parsing.attribute.value.expected=Attribute value expected
|
||||
xml.parsing.bad.character=Bad character
|
||||
xml.parsing.closing.tag.is.not.done=Closing tag is not valid
|
||||
xml.parsing.closing.tag.matches.nothing=Closing tag matches nothing
|
||||
xml.parsing.closing.tag.name.missing=Closing tag name is missing
|
||||
xml.parsing.expected.attribute.eq.sign== expected
|
||||
xml.parsing.named.element.is.not.closed=Element {0} is not closed
|
||||
xml.parsing.multiple.root.tags=Multiple root tags
|
||||
xml.parsing.processing.instruction.name.expected=Processing instruction name expected
|
||||
xml.parsing.tag.name.expected=Tag name expected
|
||||
xml.parsing.tag.start.is.not.closed=Tag start is not closed
|
||||
xml.parsing.top.level.element.is.not.completed=Top level element is not completed
|
||||
xml.parsing.unclosed.attribute.value=Attribute value is not closed
|
||||
xml.parsing.unescaped.ampersand.or.nonterminated.character.entity.reference=Unescaped \\& or nonterminated character/entity reference
|
||||
xml.parsing.unexpected.end.of.file=Unexpected end of file
|
||||
xml.parsing.unexpected.token=Unexpected token
|
||||
xml.parsing.unexpected.tokens=Unexpected tokens
|
||||
xml.parsing.unterminated.processing.instruction=Processing instruction not terminated
|
||||
xml.parsing.way.too.unbalanced=Way too unbalanced. Stopping attempt to balance tags properly at this point
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.xml.syntax
|
||||
|
||||
import com.intellij.platform.syntax.parser.SyntaxTreeBuilder
|
||||
|
||||
class XmlParser {
|
||||
fun parse(builder: SyntaxTreeBuilder) {
|
||||
val file = builder.mark()
|
||||
XmlParsing(builder).parseDocument()
|
||||
file.done(XmlSyntaxElementType.XML_FILE)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,501 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.xml.syntax
|
||||
|
||||
import com.intellij.openapi.util.NlsContexts
|
||||
import com.intellij.platform.syntax.SyntaxElementType
|
||||
import com.intellij.platform.syntax.element.SyntaxTokenTypes
|
||||
import com.intellij.platform.syntax.parser.SyntaxTreeBuilder
|
||||
import com.intellij.xml.syntax.XmlSyntaxElementType.XML_ATTRIBUTE
|
||||
import com.intellij.xml.syntax.XmlSyntaxElementType.XML_ATTRIBUTE_VALUE
|
||||
import com.intellij.xml.syntax.XmlSyntaxElementType.XML_CDATA
|
||||
import com.intellij.xml.syntax.XmlSyntaxElementType.XML_COMMENT
|
||||
import com.intellij.xml.syntax.XmlSyntaxElementType.XML_DOCTYPE
|
||||
import com.intellij.xml.syntax.XmlSyntaxElementType.XML_DOCUMENT
|
||||
import com.intellij.xml.syntax.XmlSyntaxElementType.XML_ENTITY_REF
|
||||
import com.intellij.xml.syntax.XmlSyntaxElementType.XML_PROCESSING_INSTRUCTION
|
||||
import com.intellij.xml.syntax.XmlSyntaxElementType.XML_PROLOG
|
||||
import com.intellij.xml.syntax.XmlSyntaxElementType.XML_TAG
|
||||
import com.intellij.xml.syntax.XmlSyntaxElementType.XML_TEXT
|
||||
import com.intellij.xml.syntax.XmlSyntaxTokenType.XML_ATTRIBUTE_VALUE_END_DELIMITER
|
||||
import com.intellij.xml.syntax.XmlSyntaxTokenType.XML_ATTRIBUTE_VALUE_START_DELIMITER
|
||||
import com.intellij.xml.syntax.XmlSyntaxTokenType.XML_BAD_CHARACTER
|
||||
import com.intellij.xml.syntax.XmlSyntaxTokenType.XML_CDATA_END
|
||||
import com.intellij.xml.syntax.XmlSyntaxTokenType.XML_CDATA_START
|
||||
import com.intellij.xml.syntax.XmlSyntaxTokenType.XML_CHAR_ENTITY_REF
|
||||
import com.intellij.xml.syntax.XmlSyntaxTokenType.XML_COMMENT_CHARACTERS
|
||||
import com.intellij.xml.syntax.XmlSyntaxTokenType.XML_COMMENT_END
|
||||
import com.intellij.xml.syntax.XmlSyntaxTokenType.XML_COMMENT_START
|
||||
import com.intellij.xml.syntax.XmlSyntaxTokenType.XML_CONDITIONAL_COMMENT_END
|
||||
import com.intellij.xml.syntax.XmlSyntaxTokenType.XML_CONDITIONAL_COMMENT_END_START
|
||||
import com.intellij.xml.syntax.XmlSyntaxTokenType.XML_CONDITIONAL_COMMENT_START
|
||||
import com.intellij.xml.syntax.XmlSyntaxTokenType.XML_CONDITIONAL_COMMENT_START_END
|
||||
import com.intellij.xml.syntax.XmlSyntaxTokenType.XML_DOCTYPE_END
|
||||
import com.intellij.xml.syntax.XmlSyntaxTokenType.XML_DOCTYPE_START
|
||||
import com.intellij.xml.syntax.XmlSyntaxTokenType.XML_EMPTY_ELEMENT_END
|
||||
import com.intellij.xml.syntax.XmlSyntaxTokenType.XML_END_TAG_START
|
||||
import com.intellij.xml.syntax.XmlSyntaxTokenType.XML_ENTITY_REF_TOKEN
|
||||
import com.intellij.xml.syntax.XmlSyntaxTokenType.XML_EQ
|
||||
import com.intellij.xml.syntax.XmlSyntaxTokenType.XML_NAME
|
||||
import com.intellij.xml.syntax.XmlSyntaxTokenType.XML_PI_END
|
||||
import com.intellij.xml.syntax.XmlSyntaxTokenType.XML_PI_START
|
||||
import com.intellij.xml.syntax.XmlSyntaxTokenType.XML_REAL_WHITE_SPACE
|
||||
import com.intellij.xml.syntax.XmlSyntaxTokenType.XML_START_TAG_START
|
||||
import com.intellij.xml.syntax.XmlSyntaxTokenType.XML_TAG_CHARACTERS
|
||||
import com.intellij.xml.syntax.XmlSyntaxTokenType.XML_TAG_END
|
||||
|
||||
open class XmlParsing(
|
||||
@JvmField protected val myBuilder: SyntaxTreeBuilder,
|
||||
) {
|
||||
private val tagNamesStack = ArrayDeque<String>()
|
||||
|
||||
fun parseDocument() {
|
||||
val document = mark()
|
||||
|
||||
while (isCommentToken(token())) {
|
||||
parseComment()
|
||||
}
|
||||
|
||||
parseProlog()
|
||||
|
||||
var rootTagCount = 0
|
||||
var error: SyntaxTreeBuilder.Marker? = null
|
||||
|
||||
fun flushError() {
|
||||
error?.let {
|
||||
it.error(XmlSyntaxBundle.message("xml.parsing.unexpected.tokens"))
|
||||
error = null
|
||||
}
|
||||
}
|
||||
|
||||
while (!eof()) {
|
||||
val tt = token()
|
||||
when {
|
||||
tt === XML_START_TAG_START -> {
|
||||
flushError()
|
||||
rootTagCount++
|
||||
parseTag(rootTagCount > 1)
|
||||
}
|
||||
isCommentToken(tt) -> {
|
||||
flushError()
|
||||
parseComment()
|
||||
}
|
||||
tt === XML_PI_START -> {
|
||||
flushError()
|
||||
parseProcessingInstruction()
|
||||
}
|
||||
tt === XML_REAL_WHITE_SPACE -> {
|
||||
flushError()
|
||||
advance()
|
||||
}
|
||||
else -> {
|
||||
if (error == null) error = mark()
|
||||
advance()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
error?.error(XmlSyntaxBundle.message("xml.parsing.top.level.element.is.not.completed"))
|
||||
|
||||
if (rootTagCount == 0) {
|
||||
val rootTag = mark()
|
||||
mark().error(XmlSyntaxBundle.message("xml.parsing.absent.root.tag"))
|
||||
rootTag.done(XML_TAG)
|
||||
}
|
||||
|
||||
document.done(XML_DOCUMENT)
|
||||
}
|
||||
|
||||
private fun parseDoctype() {
|
||||
checkCurrentToken(XML_DOCTYPE_START) { "Doctype start expected" }
|
||||
val doctype = mark()
|
||||
advance()
|
||||
|
||||
while (token() !== XML_DOCTYPE_END && !eof()) advance()
|
||||
if (eof()) {
|
||||
error(XmlSyntaxBundle.message("xml.parsing.unexpected.end.of.file"))
|
||||
}
|
||||
else {
|
||||
advance()
|
||||
}
|
||||
|
||||
doctype.done(XML_DOCTYPE)
|
||||
}
|
||||
|
||||
protected fun parseTag(multipleRootTagError: Boolean) {
|
||||
checkCurrentToken(XML_START_TAG_START) { "Tag start expected" }
|
||||
val tag = mark()
|
||||
|
||||
val tagName = parseTagHeader(multipleRootTagError, tag) ?: return
|
||||
|
||||
val content = mark()
|
||||
parseTagContent()
|
||||
|
||||
if (token() === XML_END_TAG_START) {
|
||||
val footer = mark()
|
||||
advance()
|
||||
|
||||
if (token() === XML_NAME) {
|
||||
val endName = myBuilder.tokenText
|
||||
if (tagName != endName && endName in tagNamesStack) {
|
||||
footer.rollbackTo()
|
||||
tagNamesStack.removeLast()
|
||||
tag.doneBefore(XML_TAG, content, XmlSyntaxBundle.message("xml.parsing.named.element.is.not.closed", tagName))
|
||||
content.drop()
|
||||
return
|
||||
}
|
||||
|
||||
advance()
|
||||
}
|
||||
else {
|
||||
error(XmlSyntaxBundle.message("xml.parsing.closing.tag.name.missing"))
|
||||
}
|
||||
footer.drop()
|
||||
|
||||
while (token() !== XML_TAG_END
|
||||
&& token() !== XML_START_TAG_START
|
||||
&& token() !== XML_END_TAG_START
|
||||
&& !eof()
|
||||
) {
|
||||
error(XmlSyntaxBundle.message("xml.parsing.unexpected.token"))
|
||||
advance()
|
||||
}
|
||||
|
||||
if (token() === XML_TAG_END) {
|
||||
advance()
|
||||
}
|
||||
else {
|
||||
error(XmlSyntaxBundle.message("xml.parsing.closing.tag.is.not.done"))
|
||||
}
|
||||
}
|
||||
else {
|
||||
error(XmlSyntaxBundle.message("xml.parsing.unexpected.end.of.file"))
|
||||
}
|
||||
|
||||
content.drop()
|
||||
tagNamesStack.removeLast()
|
||||
tag.done(XML_TAG)
|
||||
}
|
||||
|
||||
private fun parseTagHeader(multipleRootTagError: Boolean, tag: SyntaxTreeBuilder.Marker): String? {
|
||||
if (multipleRootTagError) {
|
||||
val error = mark()
|
||||
advance()
|
||||
error.error(XmlSyntaxBundle.message("xml.parsing.multiple.root.tags"))
|
||||
}
|
||||
else {
|
||||
advance()
|
||||
}
|
||||
|
||||
val tagName: String?
|
||||
if (token() !== XML_NAME || myBuilder.rawLookup(-1) === SyntaxTokenTypes.WHITE_SPACE) {
|
||||
error(XmlSyntaxBundle.message("xml.parsing.tag.name.expected"))
|
||||
tagName = ""
|
||||
}
|
||||
else {
|
||||
tagName = myBuilder.tokenText
|
||||
checkNotNull(tagName)
|
||||
advance()
|
||||
}
|
||||
tagNamesStack.addLast(tagName)
|
||||
|
||||
while (true) {
|
||||
val tt = token()
|
||||
when {
|
||||
tt === XML_NAME -> parseAttribute()
|
||||
tt === XML_CHAR_ENTITY_REF || tt === XML_ENTITY_REF_TOKEN -> parseReference()
|
||||
else -> break
|
||||
}
|
||||
}
|
||||
|
||||
if (token() === XML_EMPTY_ELEMENT_END) {
|
||||
advance()
|
||||
tagNamesStack.removeLast()
|
||||
tag.done(XML_TAG)
|
||||
return null
|
||||
}
|
||||
|
||||
if (token() === XML_TAG_END) {
|
||||
advance()
|
||||
}
|
||||
else {
|
||||
error(XmlSyntaxBundle.message("xml.parsing.tag.start.is.not.closed"))
|
||||
tagNamesStack.removeLast()
|
||||
tag.done(XML_TAG)
|
||||
return null
|
||||
}
|
||||
|
||||
if (tagNamesStack.size > BALANCING_DEPTH_THRESHOLD) {
|
||||
error(XmlSyntaxBundle.message("xml.parsing.way.too.unbalanced"))
|
||||
tag.done(XML_TAG)
|
||||
return null
|
||||
}
|
||||
|
||||
return tagName
|
||||
}
|
||||
|
||||
fun parseTagContent() {
|
||||
var xmlText: SyntaxTreeBuilder.Marker? = null
|
||||
|
||||
fun terminateText() {
|
||||
xmlText?.let {
|
||||
it.done(XML_TEXT)
|
||||
xmlText = null
|
||||
}
|
||||
}
|
||||
|
||||
fun startText() {
|
||||
if (xmlText == null) xmlText = mark()
|
||||
}
|
||||
|
||||
while (true) {
|
||||
val tt = token()
|
||||
if (tt == null || tt === XML_END_TAG_START) {
|
||||
break
|
||||
}
|
||||
|
||||
if (tt === XML_START_TAG_START) {
|
||||
terminateText()
|
||||
parseTag(false)
|
||||
}
|
||||
else if (tt === XML_PI_START) {
|
||||
terminateText()
|
||||
parseProcessingInstruction()
|
||||
}
|
||||
else if (tt === XML_ENTITY_REF_TOKEN) {
|
||||
terminateText()
|
||||
parseReference()
|
||||
}
|
||||
else if (tt === XML_CHAR_ENTITY_REF) {
|
||||
startText()
|
||||
parseReference()
|
||||
}
|
||||
else if (tt === XML_CDATA_START) {
|
||||
startText()
|
||||
parseCData()
|
||||
}
|
||||
else if (isCommentToken(tt)) {
|
||||
terminateText()
|
||||
parseComment()
|
||||
}
|
||||
else if (tt === XML_BAD_CHARACTER) {
|
||||
startText()
|
||||
val error = mark()
|
||||
advance()
|
||||
error.error(XmlSyntaxBundle.message("xml.parsing.unescaped.ampersand.or.nonterminated.character.entity.reference"))
|
||||
}
|
||||
//else if (tt is ICustomParsingType || tt is ILazyParseableElementType) { TODO figure out this!!!
|
||||
// terminateText()
|
||||
// advance()
|
||||
//}
|
||||
else {
|
||||
startText()
|
||||
advance()
|
||||
}
|
||||
}
|
||||
|
||||
terminateText()
|
||||
}
|
||||
|
||||
protected open fun isCommentToken(tt: SyntaxElementType?): Boolean {
|
||||
return tt === XML_COMMENT_START
|
||||
}
|
||||
|
||||
protected fun mark(): SyntaxTreeBuilder.Marker {
|
||||
return myBuilder.mark()
|
||||
}
|
||||
|
||||
private fun parseCData() {
|
||||
checkCurrentToken(XML_CDATA_START)
|
||||
val cdata = mark()
|
||||
while (token() !== XML_CDATA_END && !eof()) {
|
||||
advance()
|
||||
}
|
||||
|
||||
if (!eof()) {
|
||||
advance()
|
||||
}
|
||||
|
||||
cdata.done(XML_CDATA)
|
||||
}
|
||||
|
||||
protected open fun parseComment() {
|
||||
val comment = mark()
|
||||
advance()
|
||||
while (true) {
|
||||
val tt = token()
|
||||
if (tt === XML_COMMENT_CHARACTERS
|
||||
|| tt === XML_CONDITIONAL_COMMENT_START
|
||||
|| tt === XML_CONDITIONAL_COMMENT_START_END
|
||||
|| tt === XML_CONDITIONAL_COMMENT_END_START
|
||||
|| tt === XML_CONDITIONAL_COMMENT_END
|
||||
) {
|
||||
advance()
|
||||
continue
|
||||
}
|
||||
else if (tt === XML_BAD_CHARACTER) {
|
||||
val error = mark()
|
||||
advance()
|
||||
error.error(XmlSyntaxBundle.message("xml.parsing.bad.character"))
|
||||
continue
|
||||
}
|
||||
if (tt === XML_COMMENT_END) {
|
||||
advance()
|
||||
}
|
||||
break
|
||||
}
|
||||
comment.done(XML_COMMENT)
|
||||
}
|
||||
|
||||
private fun parseReference() {
|
||||
val tt = token()
|
||||
when {
|
||||
tt === XML_CHAR_ENTITY_REF -> {
|
||||
advance()
|
||||
}
|
||||
tt === XML_ENTITY_REF_TOKEN -> {
|
||||
val ref = mark()
|
||||
advance()
|
||||
ref.done(XML_ENTITY_REF)
|
||||
}
|
||||
else -> {
|
||||
kotlin.error("Unexpected token: $tt")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseAttribute() {
|
||||
checkCurrentToken(XML_NAME)
|
||||
val att = mark()
|
||||
advance()
|
||||
if (token() === XML_EQ) {
|
||||
advance()
|
||||
parseAttributeValue()
|
||||
}
|
||||
else {
|
||||
error(XmlSyntaxBundle.message("xml.parsing.expected.attribute.eq.sign"))
|
||||
}
|
||||
att.done(XML_ATTRIBUTE)
|
||||
}
|
||||
|
||||
private fun parseAttributeValue() {
|
||||
val attValue = mark()
|
||||
if (token() === XML_ATTRIBUTE_VALUE_START_DELIMITER) {
|
||||
while (true) {
|
||||
val tt = token()
|
||||
if (tt == null
|
||||
|| tt === XML_ATTRIBUTE_VALUE_END_DELIMITER
|
||||
|| tt === XML_END_TAG_START
|
||||
|| tt === XML_EMPTY_ELEMENT_END
|
||||
|| tt === XML_START_TAG_START
|
||||
) {
|
||||
break
|
||||
}
|
||||
|
||||
if (tt === XML_BAD_CHARACTER) {
|
||||
val error = mark()
|
||||
advance()
|
||||
error.error(XmlSyntaxBundle.message("xml.parsing.unescaped.ampersand.or.nonterminated.character.entity.reference"))
|
||||
}
|
||||
else if (tt === XML_ENTITY_REF_TOKEN) {
|
||||
parseReference()
|
||||
}
|
||||
else {
|
||||
advance()
|
||||
}
|
||||
}
|
||||
|
||||
if (token() === XML_ATTRIBUTE_VALUE_END_DELIMITER) {
|
||||
advance()
|
||||
}
|
||||
else {
|
||||
error(XmlSyntaxBundle.message("xml.parsing.unclosed.attribute.value"))
|
||||
}
|
||||
}
|
||||
else {
|
||||
error(XmlSyntaxBundle.message("xml.parsing.attribute.value.expected"))
|
||||
}
|
||||
|
||||
attValue.done(XML_ATTRIBUTE_VALUE)
|
||||
}
|
||||
|
||||
private fun parseProlog() {
|
||||
val prolog = mark()
|
||||
while (true) {
|
||||
val tt = token()
|
||||
when {
|
||||
tt === XML_PI_START -> parseProcessingInstruction()
|
||||
tt === XML_DOCTYPE_START -> parseDoctype()
|
||||
isCommentToken(tt) -> parseComment()
|
||||
tt === XML_REAL_WHITE_SPACE -> advance()
|
||||
else -> break
|
||||
}
|
||||
}
|
||||
prolog.done(XML_PROLOG)
|
||||
}
|
||||
|
||||
private fun parseProcessingInstruction() {
|
||||
checkCurrentToken(XML_PI_START)
|
||||
val pi = mark()
|
||||
advance()
|
||||
if (token() !== XML_NAME) {
|
||||
error(XmlSyntaxBundle.message("xml.parsing.processing.instruction.name.expected"))
|
||||
}
|
||||
else {
|
||||
advance()
|
||||
}
|
||||
|
||||
val tokenType = token()
|
||||
if (tokenType === XML_TAG_CHARACTERS) {
|
||||
while (token() === XML_TAG_CHARACTERS) {
|
||||
advance()
|
||||
}
|
||||
}
|
||||
else {
|
||||
while (token() === XML_NAME) {
|
||||
advance()
|
||||
if (token() === XML_EQ) {
|
||||
advance()
|
||||
}
|
||||
else {
|
||||
error(XmlSyntaxBundle.message("xml.parsing.expected.attribute.eq.sign"))
|
||||
}
|
||||
parseAttributeValue()
|
||||
}
|
||||
}
|
||||
|
||||
if (token() === XML_PI_END) {
|
||||
advance()
|
||||
}
|
||||
else {
|
||||
error(XmlSyntaxBundle.message("xml.parsing.unterminated.processing.instruction"))
|
||||
}
|
||||
|
||||
pi.done(XML_PROCESSING_INSTRUCTION)
|
||||
}
|
||||
|
||||
protected fun token(): SyntaxElementType? {
|
||||
return myBuilder.tokenType
|
||||
}
|
||||
|
||||
protected fun eof(): Boolean {
|
||||
return myBuilder.eof()
|
||||
}
|
||||
|
||||
protected fun advance() {
|
||||
myBuilder.advanceLexer()
|
||||
}
|
||||
|
||||
private fun error(message: @NlsContexts.ParsingError String) {
|
||||
myBuilder.error(message)
|
||||
}
|
||||
|
||||
private fun checkCurrentToken(type: SyntaxElementType) {
|
||||
check(token() === type) { "Expected: $type, got: ${token()}" }
|
||||
}
|
||||
|
||||
private inline fun checkCurrentToken(type: SyntaxElementType, error: () -> String) {
|
||||
check(token() === type, error)
|
||||
}
|
||||
}
|
||||
|
||||
private const val BALANCING_DEPTH_THRESHOLD = 1000
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.xml.syntax
|
||||
|
||||
import com.intellij.platform.syntax.i18n.ResourceBundle
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import org.jetbrains.annotations.Nls
|
||||
import org.jetbrains.annotations.NonNls
|
||||
import org.jetbrains.annotations.PropertyKey
|
||||
import org.jetbrains.annotations.VisibleForTesting
|
||||
|
||||
object XmlSyntaxBundle {
|
||||
const val BUNDLE: @NonNls String = "messages.XmlSyntaxBundle"
|
||||
|
||||
val resourceBundle: ResourceBundle = run {
|
||||
val defaultMapping by lazy { DefaultXmlSyntaxResources.mappings }
|
||||
ResourceBundle(
|
||||
bundleClass = "com.intellij.xml.syntax.XmlSyntaxBundle",
|
||||
pathToBundle = BUNDLE,
|
||||
self = this,
|
||||
defaultMapping = defaultMapping
|
||||
)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun message(key: @PropertyKey(resourceBundle = BUNDLE) String, vararg params: Any): @Nls String {
|
||||
return resourceBundle.message(key, *params)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun messagePointer(key: @PropertyKey(resourceBundle = BUNDLE) String, vararg params: Any): () -> @Nls String {
|
||||
return resourceBundle.messagePointer(key, *params)
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@ApiStatus.Internal
|
||||
object DefaultXmlSyntaxResourcesTestAccessor {
|
||||
val mappings: Map<String, String> get() = DefaultXmlSyntaxResources.mappings
|
||||
val defaultJavaSyntaxResourcesName: String get() = DefaultXmlSyntaxResources::class.qualifiedName!!
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.xml.syntax
|
||||
|
||||
import com.intellij.platform.syntax.SyntaxElementTypeSet
|
||||
import com.intellij.platform.syntax.element.SyntaxTokenTypes
|
||||
import com.intellij.platform.syntax.lexer.Lexer
|
||||
import com.intellij.platform.syntax.syntaxElementTypeSetOf
|
||||
import com.intellij.xml.syntax.lexer.XmlLexer
|
||||
|
||||
object XmlSyntaxDefinition {
|
||||
val WHITESPACES: SyntaxElementTypeSet = syntaxElementTypeSetOf(SyntaxTokenTypes.WHITE_SPACE)
|
||||
|
||||
val COMMENTS: SyntaxElementTypeSet = syntaxElementTypeSetOf(
|
||||
XmlSyntaxTokenType.XML_COMMENT_START,
|
||||
XmlSyntaxTokenType.XML_COMMENT_CHARACTERS,
|
||||
XmlSyntaxTokenType.XML_COMMENT_END,
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun createLexer(): Lexer = XmlLexer()
|
||||
}
|
||||
Reference in New Issue
Block a user