IJPL-163136 introduce syntax XML parser

GitOrigin-RevId: 2b3cd5542d092d3eec1e6646f6003707f6ccdfd5
This commit is contained in:
Max Medvedev
2025-08-21 18:12:27 +00:00
committed by intellij-monorepo-bot
parent 77b888bda1
commit 05fa043467
21 changed files with 742 additions and 3 deletions
@@ -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);
}
+1
View File
@@ -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 {
+2
View File
@@ -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()
}
}
+2
View File
@@ -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()
}