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}