001package org.jsoup.nodes;
002
003import org.jsoup.Connection;
004import org.jsoup.Jsoup;
005import org.jsoup.helper.DataUtil;
006import org.jsoup.helper.Validate;
007import org.jsoup.internal.QuietAppendable;
008import org.jsoup.internal.StringUtil;
009import org.jsoup.parser.ParseSettings;
010import org.jsoup.parser.Parser;
011import org.jsoup.parser.Tag;
012import org.jsoup.select.Elements;
013import org.jsoup.select.Evaluator;
014import org.jsoup.select.Selector;
015import org.jspecify.annotations.Nullable;
016
017import java.nio.charset.Charset;
018import java.util.List;
019
020import static org.jsoup.parser.Parser.NamespaceHtml;
021
022/**
023 A HTML Document.
024
025 @author Jonathan Hedley, jonathan@hedley.net */
026public class Document extends Element {
027    private @Nullable Connection connection; // the connection this doc was fetched from, if any
028    private OutputSettings outputSettings = new OutputSettings();
029    private Parser parser; // the parser used to parse this document
030    private QuirksMode quirksMode = QuirksMode.noQuirks;
031    private final String location;
032
033    /**
034     Create a new, empty Document, in the specified namespace.
035     @param namespace the namespace of this Document's root node.
036     @param baseUri base URI of document
037     @see org.jsoup.Jsoup#parse
038     @see #createShell
039     */
040    public Document(String namespace, String baseUri) {
041        this(namespace, baseUri, Parser.htmlParser()); // default HTML parser, but overridable
042    }
043
044    private Document(String namespace, String baseUri, Parser parser) {
045        super(new Tag("#root", namespace), baseUri);
046        this.location = baseUri;
047        this.parser = parser;
048    }
049
050    /**
051     Create a new, empty Document, in the HTML namespace.
052     @param baseUri base URI of document
053     @see org.jsoup.Jsoup#parse
054     @see #Document(String namespace, String baseUri)
055     */
056    public Document(String baseUri) {
057        this(NamespaceHtml, baseUri);
058    }
059
060    /**
061     Create a valid, empty shell of an HTML document, suitable for adding more elements to.
062     @param baseUri baseUri of document
063     @return document with html, head, and body elements.
064     */
065    public static Document createShell(String baseUri) {
066        Validate.notNull(baseUri);
067
068        Document doc = new Document(baseUri);
069        Element html = doc.appendElement("html");
070        html.appendElement("head");
071        html.appendElement("body");
072
073        return doc;
074    }
075
076    /**
077     * Get the URL this Document was parsed from. If the starting URL is a redirect,
078     * this will return the final URL from which the document was served from.
079     * <p>Will return an empty string if the location is unknown (e.g. if parsed from a String).
080     * @return location
081     */
082    public String location() {
083        return location;
084    }
085
086    /**
087     Returns the Connection (Request/Response) object that was used to fetch this document, if any; otherwise, a new
088     default Connection object. This can be used to continue a session, preserving settings and cookies, etc.
089     @return the Connection (session) associated with this Document, or an empty one otherwise.
090     @see Connection#newRequest()
091     */
092    public Connection connection() {
093        if (connection == null)
094            return Jsoup.newSession();
095        else
096            return connection;
097    }
098
099    /**
100     * Returns this Document's doctype.
101     * @return document type, or null if not set
102     */
103    public @Nullable DocumentType documentType() {
104        for (Node node : childNodes) {
105            if (node instanceof DocumentType)
106                return (DocumentType) node;
107            else if (!(node instanceof LeafNode)) // scans forward across comments, text, processing instructions etc
108                break;
109        }
110        return null;
111    }
112
113    /**
114     Find the root HTML element, or create it if it doesn't exist.
115     @return the root HTML element.
116     */
117    private Element htmlEl() {
118        Element el = firstElementChild();
119        while (el != null) {
120            if (el.nameIs("html"))
121                return el;
122            el = el.nextElementSibling();
123        }
124        return appendElement("html");
125    }
126
127    /**
128     Get this document's {@code head} element.
129     <p>
130     As a side effect, if this Document does not already have an HTML structure, it will be created. If you do not want
131     that, use {@code #selectFirst("head")} instead.
132
133     @return {@code head} element.
134     */
135    public Element head() {
136        final Element html = htmlEl();
137        Element el = html.firstElementChild();
138        while (el != null) {
139            if (el.nameIs("head"))
140                return el;
141            el = el.nextElementSibling();
142        }
143        return html.prependElement("head");
144    }
145
146    /**
147     Get this document's {@code <body>} or {@code <frameset>} element.
148     <p>
149     As a <b>side-effect</b>, if this Document does not already have an HTML structure, it will be created with a {@code
150    <body>} element. If you do not want that, use {@code #selectFirst("body")} instead.
151
152     @return {@code body} element for documents with a {@code <body>}, a new {@code <body>} element if the document
153     had no contents, or the outermost {@code <frameset> element} for frameset documents.
154     */
155    public Element body() {
156        final Element html = htmlEl();
157        Element el = html.firstElementChild();
158        while (el != null) {
159            if (el.nameIs("body") || el.nameIs("frameset"))
160                return el;
161            el = el.nextElementSibling();
162        }
163        return html.appendElement("body");
164    }
165
166    /**
167     Get each of the {@code <form>} elements contained in this document.
168     @return a List of FormElement objects, which will be empty if there are none.
169     @see Elements#forms()
170     @see FormElement#elements()
171     @since 1.15.4
172     */
173    public List<FormElement> forms() {
174        return select("form").forms();
175    }
176
177    /**
178     Selects the first {@link FormElement} in this document that matches the query. If none match, throws an
179     {@link IllegalArgumentException}.
180     @param cssQuery a {@link Selector} CSS query
181     @return the first matching {@code <form>} element
182     @throws IllegalArgumentException if no match is found
183     @since 1.15.4
184     */
185    public FormElement expectForm(String cssQuery) {
186        Elements els = select(cssQuery);
187        for (Element el : els) {
188            if (el instanceof FormElement) return (FormElement) el;
189        }
190        Validate.fail("No form elements matched the query '%s' in the document.", cssQuery);
191        return null; // (not really)
192    }
193
194    /**
195     Get the string contents of the document's {@code title} element.
196     @return Trimmed title, or empty string if none set.
197     */
198    public String title() {
199        // title is a preserve whitespace tag (for document output), but normalised here
200        Element titleEl = head().selectFirst(titleEval);
201        return titleEl != null ? StringUtil.normaliseWhitespace(titleEl.text()).trim() : "";
202    }
203    private static final Evaluator titleEval = new Evaluator.Tag("title");
204
205    /**
206     Set the document's {@code title} element. Updates the existing element, or adds {@code title} to {@code head} if
207     not present
208     @param title string to set as title
209     */
210    public void title(String title) {
211        Validate.notNull(title);
212        Element titleEl = head().selectFirst(titleEval);
213        if (titleEl == null) // add to head
214            titleEl = head().appendElement("title");
215        titleEl.text(title);
216    }
217
218    /**
219     Create a new Element, with this document's base uri. Does not make the new element a child of this document.
220     @param tagName element tag name (e.g. {@code a})
221     @return new element
222     */
223    public Element createElement(String tagName) {
224        return new Element(
225            parser.tagSet().valueOf(tagName, parser.defaultNamespace(), ParseSettings.preserveCase),
226            searchUpForAttribute(this, BaseUriKey)
227        );
228    }
229
230    /** Append the HTML of this Document to the supplied {@link QuietAppendable}. */
231    @Override
232    protected void outerHtml(QuietAppendable accum) {
233        html(accum);
234    }
235
236    /**
237     Set the text of the {@code body} of this document. Any existing nodes within the body will be cleared.
238     @param text un-encoded text
239     @return this document
240     */
241    @Override
242    public Element text(String text) {
243        body().text(text); // overridden to not nuke doc structure
244        return this;
245    }
246
247    @Override
248    public String nodeName() {
249        return "#document";
250    }
251
252    /**
253     Set the output character set of this Document. This method is equivalent to
254     {@link OutputSettings#charset(java.nio.charset.Charset) OutputSettings.charset(Charset)}, but additionally adds or
255     updates the charset / encoding element within the Document.
256
257     <p>If there's no existing element with charset / encoding information yet, one will
258     be created. Obsolete charset / encoding definitions are removed.</p>
259
260     <p><b>Elements used:</b></p>
261
262     <ul>
263     <li><b>HTML:</b> <i>&lt;meta charset="CHARSET"&gt;</i></li>
264     <li><b>XML:</b> <i>&lt;?xml version="1.0" encoding="CHARSET"&gt;</i></li>
265     </ul>
266
267     @param charset Charset
268     @see OutputSettings#charset(java.nio.charset.Charset)
269     */
270    public void charset(Charset charset) {
271        outputSettings.charset(charset);
272        ensureMetaCharsetElement();
273    }
274
275    /**
276     Get the output character set of this Document. This method is equivalent to {@link OutputSettings#charset()}.
277
278     @return the current Charset
279     @see OutputSettings#charset()
280     */
281    public Charset charset() {
282        return outputSettings.charset();
283    }
284
285    @Override
286    public Document clone() {
287        Document clone = (Document) super.clone();
288        if (attributes != null) clone.attributes = attributes.clone();
289        clone.outputSettings = this.outputSettings.clone();
290        // parser is pointer copy
291        return clone;
292    }
293
294    @Override
295    public Document shallowClone() {
296        Document clone = new Document(this.tag().namespace(), baseUri(), parser); // preserves parser pointer
297        if (attributes != null) clone.attributes = attributes.clone();
298        clone.outputSettings = this.outputSettings.clone();
299        return clone;
300    }
301    
302
303    private void ensureMetaCharsetElement() {
304        OutputSettings.Syntax syntax = outputSettings().syntax();
305
306        if (syntax == OutputSettings.Syntax.html) {
307            Element metaCharset = selectFirst("meta[charset]");
308            if (metaCharset != null) {
309                metaCharset.attr("charset", charset().displayName());
310            } else {
311                head().appendElement("meta").attr("charset", charset().displayName());
312            }
313            select("meta[name=charset]").remove(); // Remove obsolete elements
314        } else if (syntax == OutputSettings.Syntax.xml) {
315            XmlDeclaration decl = ensureXmlDecl();
316            decl.attr("version", "1.0");
317            decl.attr("encoding", charset().displayName());
318        }
319    }
320
321    private XmlDeclaration ensureXmlDecl() {
322        Node node = firstChild();
323        if (node instanceof XmlDeclaration) {
324            XmlDeclaration decl = (XmlDeclaration) node;
325            if (decl.name().equals("xml")) return decl;
326        }
327        XmlDeclaration decl = new XmlDeclaration("xml", false);
328        prependChild(decl);
329        return decl;
330    }
331
332
333    /**
334     * A Document's output settings control the form of the text() and html() methods.
335     */
336    public static class OutputSettings implements Cloneable {
337        /**
338         * The output serialization syntax.
339         */
340        public enum Syntax {html, xml}
341        private Entities.EscapeMode escapeMode = Entities.EscapeMode.base;
342        private Charset charset = DataUtil.UTF_8;
343        private boolean prettyPrint = true;
344        private boolean outline = false;
345        private int indentAmount = 1;
346        private int maxPaddingWidth = 30;
347        private Syntax syntax = Syntax.html;
348
349        /**
350         Create a new OutputSettings object, with the default settings (UTF-8, HTML, EscapeMode.base, pretty-printing,
351         indent amount of 1).
352         */
353        public OutputSettings() {
354        }
355
356        /**
357         Get the document's current entity escape mode:
358         <ul>
359         <li><code>xhtml</code>, the minimal named entities in XHTML / XML</li>
360         <li><code>base</code>, which provides a limited set of named HTML
361         entities and escapes other characters as numbered entities for maximum compatibility</li>
362         <li><code>extended</code>,
363         which uses the complete set of HTML named entities.</li>
364         </ul>
365         <p>The default escape mode is <code>base</code>.
366         @return the document's current escape mode
367         */
368        public Entities.EscapeMode escapeMode() {
369            return escapeMode;
370        }
371
372        /**
373         * Set the document's escape mode, which determines how characters are escaped when the output character set
374         * does not support a given character:- using either a named or a numbered escape.
375         * @param escapeMode the new escape mode to use
376         * @return the document's output settings, for chaining
377         */
378        public OutputSettings escapeMode(Entities.EscapeMode escapeMode) {
379            this.escapeMode = escapeMode;
380            return this;
381        }
382
383        /**
384         * Get the document's current output charset, which is used to control which characters are escaped when
385         * generating HTML (via the <code>html()</code> methods), and which are kept intact.
386         * <p>
387         * Where possible (when parsing from a URL or File), the document's output charset is automatically set to the
388         * input charset. Otherwise, it defaults to UTF-8.
389         * @return the document's current charset.
390         */
391        public Charset charset() {
392            return charset;
393        }
394
395        /**
396         * Update the document's output charset.
397         * @param charset the new charset to use.
398         * @return the document's output settings, for chaining
399         */
400        public OutputSettings charset(Charset charset) {
401            this.charset = charset;
402            return this;
403        }
404
405        /**
406         * Update the document's output charset.
407         * @param charset the new charset (by name) to use.
408         * @return the document's output settings, for chaining
409         */
410        public OutputSettings charset(String charset) {
411            charset(Charset.forName(charset));
412            return this;
413        }
414
415        /**
416         * Get the document's current output syntax.
417         * @return current syntax
418         */
419        public Syntax syntax() {
420            return syntax;
421        }
422
423        /**
424         * Set the document's output syntax. Either {@code html}, with empty tags and boolean attributes (etc), or
425         * {@code xml}, with self-closing tags.
426         * <p>When set to {@link Document.OutputSettings.Syntax#xml xml}, the {@link #escapeMode() escapeMode} is
427         * automatically set to {@link Entities.EscapeMode#xhtml}, but may be subsequently changed if desired.</p>
428         * @param syntax serialization syntax
429         * @return the document's output settings, for chaining
430         */
431        public OutputSettings syntax(Syntax syntax) {
432            this.syntax = syntax;
433            if (syntax == Syntax.xml)
434                this.escapeMode(Entities.EscapeMode.xhtml);
435            return this;
436        }
437
438        /**
439         * Get if pretty printing is enabled. Default is true. If disabled, the HTML output methods will not re-format
440         * the output, and the output will generally look like the input.
441         * @return if pretty printing is enabled.
442         */
443        public boolean prettyPrint() {
444            return prettyPrint;
445        }
446
447        /**
448         * Enable or disable pretty printing.
449         * @param pretty new pretty print setting
450         * @return this, for chaining
451         */
452        public OutputSettings prettyPrint(boolean pretty) {
453            prettyPrint = pretty;
454            return this;
455        }
456        
457        /**
458         * Get if outline mode is enabled. Default is false. If enabled, the HTML output methods will consider
459         * all tags as block.
460         * @return if outline mode is enabled.
461         */
462        public boolean outline() {
463            return outline;
464        }
465        
466        /**
467         * Enable or disable HTML outline mode.
468         * @param outlineMode new outline setting
469         * @return this, for chaining
470         */
471        public OutputSettings outline(boolean outlineMode) {
472            outline = outlineMode;
473            return this;
474        }
475
476        /**
477         * Get the current tag indent amount, used when pretty printing.
478         * @return the current indent amount
479         */
480        public int indentAmount() {
481            return indentAmount;
482        }
483
484        /**
485         * Set the indent amount for pretty printing
486         * @param indentAmount number of spaces to use for indenting each level. Must be {@literal >=} 0.
487         * @return this, for chaining
488         */
489        public OutputSettings indentAmount(int indentAmount) {
490            Validate.isTrue(indentAmount >= 0);
491            this.indentAmount = indentAmount;
492            return this;
493        }
494
495        /**
496         * Get the current max padding amount, used when pretty printing
497         * so very deeply nested nodes don't get insane padding amounts.
498         * @return the current indent amount
499         */
500        public int maxPaddingWidth() {
501            return maxPaddingWidth;
502        }
503
504        /**
505         * Set the max padding amount for pretty printing so very deeply nested nodes don't get insane padding amounts.
506         * @param maxPaddingWidth number of spaces to use for indenting each level of nested nodes. Must be {@literal >=} -1.
507         *        Default is 30 and -1 means unlimited.
508         * @return this, for chaining
509         */
510        public OutputSettings maxPaddingWidth(int maxPaddingWidth) {
511            Validate.isTrue(maxPaddingWidth >= -1);
512            this.maxPaddingWidth = maxPaddingWidth;
513            return this;
514        }
515
516        @Override
517        public OutputSettings clone() {
518            OutputSettings clone;
519            try {
520                clone = (OutputSettings) super.clone();
521            } catch (CloneNotSupportedException e) {
522                throw new RuntimeException(e);
523            }
524            clone.charset(charset.name()); // new charset, coreCharset, and charset encoder
525            clone.escapeMode = Entities.EscapeMode.valueOf(escapeMode.name());
526            // indentAmount, maxPaddingWidth, and prettyPrint are primitives so object.clone() will handle
527            return clone;
528        }
529    }
530
531    /**
532     * Get the document's current output settings.
533     * @return the document's current output settings.
534     */
535    public OutputSettings outputSettings() {
536        return outputSettings;
537    }
538
539    /**
540     * Set the document's output settings.
541     * @param outputSettings new output settings.
542     * @return this document, for chaining.
543     */
544    public Document outputSettings(OutputSettings outputSettings) {
545        Validate.notNull(outputSettings);
546        this.outputSettings = outputSettings;
547        return this;
548    }
549
550    public enum QuirksMode {
551        noQuirks, quirks, limitedQuirks
552    }
553
554    public QuirksMode quirksMode() {
555        return quirksMode;
556    }
557
558    public Document quirksMode(QuirksMode quirksMode) {
559        this.quirksMode = quirksMode;
560        return this;
561    }
562
563    /**
564     * Get the parser that was used to parse this document.
565     * @return the parser
566     */
567    public Parser parser() {
568        return parser;
569    }
570
571    /**
572     * Set the parser used to create this document. This parser is then used when further parsing within this document
573     * is required.
574     * @param parser the configured parser to use when further parsing is required for this document.
575     * @return this document, for chaining.
576     */
577    public Document parser(Parser parser) {
578        this.parser = parser;
579        return this;
580    }
581
582    /**
583     Set the Connection used to fetch this document. This Connection is used as a session object when further requests are
584     made (e.g. when a form is submitted).
585
586     @param connection to set
587     @return this document, for chaining
588     @see Connection#newRequest()
589     @since 1.14.1
590     */
591    public Document connection(Connection connection) {
592        Validate.notNull(connection);
593        this.connection = connection;
594        return this;
595    }
596}