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}