001package org.jsoup.parser; 002 003import org.jsoup.helper.Validate; 004import org.jsoup.internal.NamespaceBindings; 005import org.jsoup.internal.SharedConstants; 006import org.jsoup.nodes.Attribute; 007import org.jsoup.nodes.Attributes; 008import org.jsoup.nodes.CDataNode; 009import org.jsoup.nodes.Comment; 010import org.jsoup.nodes.DataNode; 011import org.jsoup.nodes.Document; 012import org.jsoup.nodes.DocumentType; 013import org.jsoup.nodes.Element; 014import org.jsoup.nodes.Entities; 015import org.jsoup.nodes.LeafNode; 016import org.jsoup.nodes.Node; 017import org.jsoup.nodes.TextNode; 018import org.jsoup.nodes.XmlDeclaration; 019import org.jsoup.select.Elements; 020import org.jspecify.annotations.Nullable; 021 022import java.io.Reader; 023import java.io.StringReader; 024import java.util.HashMap; 025import java.util.List; 026import java.util.Map; 027 028import static org.jsoup.parser.Parser.NamespaceXml; 029 030/** 031 * Use the {@code XmlTreeBuilder} when you want to parse XML without any of the HTML DOM rules being applied to the 032 * document. 033 * <p>Usage example: {@code Document xmlDoc = Jsoup.parse(html, baseUrl, Parser.xmlParser());}</p> 034 * 035 * @author Jonathan Hedley 036 */ 037public class XmlTreeBuilder extends TreeBuilder { 038 final NamespaceBindings namespaceBindings = new NamespaceBindings(); 039 040 @Override ParseSettings defaultSettings() { 041 return ParseSettings.preserveCase; 042 } 043 044 @Override 045 protected void initialiseParse(Reader input, String baseUri, Parser parser) { 046 super.initialiseParse(input, baseUri, parser); 047 doc.outputSettings() 048 .syntax(Document.OutputSettings.Syntax.xml) 049 .escapeMode(Entities.EscapeMode.xhtml) 050 .prettyPrint(false); // as XML, we don't understand what whitespace is significant or not 051 052 namespaceBindings.clear(); 053 namespaceBindings.put("xml", NamespaceXml); 054 namespaceBindings.put("", NamespaceXml); 055 } 056 057 @Override 058 void initialiseParseFragment(@Nullable Element context) { 059 super.initialiseParseFragment(context); 060 if (context == null) return; 061 062 // transition to the tag's text state if available 063 TokeniserState textState = context.tag().textState(); 064 if (textState != null) tokeniser.transition(textState); 065 066 // establish the fragment's base namespace scope from the context and its ancestors, top down 067 Elements chain = context.parents(); 068 chain.add(0, context); 069 for (int i = chain.size() - 1; i >= 0; i--) { 070 Element el = chain.get(i); 071 if (el.attributesSize() > 0) { 072 namespaceBindings.applyDeclarations(el.attributes()); 073 } 074 } 075 } 076 077 Document parse(Reader input, String baseUri) { 078 return parse(input, baseUri, new Parser(this)); 079 } 080 081 Document parse(String input, String baseUri) { 082 return parse(new StringReader(input), baseUri, new Parser(this)); 083 } 084 085 @Override List<Node> completeParseFragment() { 086 return doc.childNodes(); 087 } 088 089 @Override 090 XmlTreeBuilder newInstance() { 091 return new XmlTreeBuilder(); 092 } 093 094 @Override public String defaultNamespace() { 095 return NamespaceXml; 096 } 097 098 @Override 099 TagSet defaultTagSet() { 100 return new TagSet(); // an empty tagset 101 } 102 103 @Override 104 protected boolean process(Token token) { 105 currentToken = token; 106 107 // start tag, end tag, doctype, xmldecl, comment, character, eof 108 switch (token.type) { 109 case StartTag: 110 insertElementFor(token.asStartTag()); 111 break; 112 case EndTag: 113 popStackToClose(token.asEndTag()); 114 break; 115 case Comment: 116 insertCommentFor(token.asComment()); 117 break; 118 case Character: 119 insertCharacterFor(token.asCharacter()); 120 break; 121 case Doctype: 122 insertDoctypeFor(token.asDoctype()); 123 break; 124 case XmlDecl: 125 insertXmlDeclarationFor(token.asXmlDecl()); 126 break; 127 case EOF: // could put some normalisation here if desired 128 break; 129 default: 130 Validate.fail("Unexpected token type: " + token.type); 131 } 132 return true; 133 } 134 135 void insertElementFor(Token.StartTag startTag) { 136 Attributes attributes = startTag.attributes; 137 if (attributes != null) { 138 settings.normalizeAttributes(attributes); 139 attributes.deduplicate(settings); 140 } 141 142 // close pruned namespace scopes before opening the incoming element's scope 143 enforceStackDepthLimit(); 144 namespaceBindings.pushScope(); 145 146 if (attributes != null) { 147 namespaceBindings.applyDeclarations(attributes); 148 applyNamespacesToAttributes(attributes); 149 startTag.finaliseAttributeRanges(settings); 150 } 151 152 String tagName = startTag.tagName.value(); 153 String ns = resolveNamespace(tagName); 154 Tag tag = tagFor(tagName, startTag.normalName, ns, settings); 155 Element el = new Element(tag, null, attributes); 156 currentElOrDoc().appendChild(el); 157 push(el); 158 159 if (startTag.isSelfClosing()) { 160 tag.setSeenSelfClose(); 161 pop(); // push & pop ensures onNodeInserted & onNodeClosed 162 } else if (tag.isEmpty()) { 163 pop(); // custom defined void tag 164 } else { 165 TokeniserState textState = tag.textState(); 166 if (textState != null) tokeniser.transition(textState); 167 } 168 } 169 170 /** Applies resolved namespace URIs to prefixed attributes. */ 171 private void applyNamespacesToAttributes(Attributes attributes) { 172 // collect first, then add, as userData is stored as an attribute 173 Map<String, String> attrPrefix = new HashMap<>(); 174 for (Attribute attr: attributes) { 175 if (NamespaceBindings.isDeclaration(attr.getKey())) continue; 176 String prefix = attr.prefix(); 177 if (!prefix.isEmpty()) { 178 String ns = namespaceBindings.get(prefix); 179 if (ns != null) attrPrefix.put(SharedConstants.XmlnsAttr + prefix, ns); 180 } 181 } 182 for (Map.Entry<String, String> entry : attrPrefix.entrySet()) 183 attributes.userData(entry.getKey(), entry.getValue()); 184 } 185 186 /** Resolves the namespace URI for a qualified tag name. */ 187 private String resolveNamespace(String tagName) { 188 String ns = namespaceBindings.get(""); 189 int pos = tagName.indexOf(':'); 190 if (pos > 0) { 191 String prefix = tagName.substring(0, pos); 192 String boundNamespace = namespaceBindings.get(prefix); 193 if (boundNamespace != null) 194 ns = boundNamespace; 195 } 196 return ns; 197 } 198 199 @Override 200 Element pop() { 201 namespaceBindings.popScope(); 202 return super.pop(); 203 } 204 205 void insertLeafNode(LeafNode node) { 206 currentElOrDoc().appendChild(node); 207 onNodeInserted(node); 208 } 209 210 void insertCommentFor(Token.Comment commentToken) { 211 Comment comment = new Comment(commentToken.getData()); 212 insertLeafNode(comment); 213 } 214 215 void insertCharacterFor(Token.Character token) { 216 final String data = token.getData(); 217 LeafNode node; 218 if (token.isCData()) node = new CDataNode(data); 219 else if (currentElOrDoc().tag().is(Tag.Data)) 220 node = new DataNode(data); 221 else node = new TextNode(data); 222 insertLeafNode(node); 223 } 224 225 void insertDoctypeFor(Token.Doctype token) { 226 DocumentType doctypeNode = new DocumentType(settings.normalizeTag(token.getName()), token.getPublicIdentifier(), token.getSystemIdentifier()); 227 doctypeNode.setPubSysKey(token.getPubSysKey()); 228 if (token.hasInternalSubset()) 229 doctypeNode.setInternalSubset(token.getInternalSubset()); 230 insertLeafNode(doctypeNode); 231 } 232 233 void insertXmlDeclarationFor(Token.XmlDecl token) { 234 XmlDeclaration decl = new XmlDeclaration(token.name(), token.isDeclaration); 235 if (token.attributes != null) decl.attributes().addAll(token.attributes); 236 insertLeafNode(decl); 237 } 238 239 /** 240 * If the stack contains an element with this tag's name, pop up the stack to remove the first occurrence. If not 241 * found, skips. 242 * 243 * @param endTag tag to close 244 */ 245 protected void popStackToClose(Token.EndTag endTag) { 246 String elName = settings.normalizeTag(endTag.name()); 247 Element firstFound = null; 248 249 for (int pos = stack.size() -1; pos >= 0; pos--) { 250 Element next = stack.get(pos); 251 if (next.nodeName().equals(elName)) { 252 firstFound = next; 253 break; 254 } 255 } 256 if (firstFound == null) 257 return; // not found, skip 258 259 for (int pos = stack.size() -1; pos >= 0; pos--) { 260 Element next = pop(); 261 if (next == firstFound) { 262 break; 263 } 264 } 265 } 266}