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