001package org.jsoup.nodes;
002
003import org.jsoup.internal.StringUtil;
004import org.jsoup.helper.Validate;
005import org.jsoup.nodes.Document.OutputSettings.Syntax;
006import org.jspecify.annotations.Nullable;
007
008import java.io.IOException;
009
010/**
011 * A {@code <!DOCTYPE>} node.
012 */
013public class DocumentType extends LeafNode {
014    // todo needs a bit of a chunky cleanup. this level of detail isn't needed
015    public static final String PUBLIC_KEY = "PUBLIC";
016    public static final String SYSTEM_KEY = "SYSTEM";
017    private static final String Name = "#doctype";
018    private static final String PubSysKey = "pubSysKey"; // PUBLIC or SYSTEM
019    private static final String PublicId = "publicId";
020    private static final String SystemId = "systemId";
021    // todo: quirk mode from publicId and systemId
022
023    /**
024     * Create a new doctype element.
025     * @param name the doctype's name
026     * @param publicId the doctype's public ID
027     * @param systemId the doctype's system ID
028     */
029    public DocumentType(String name, String publicId, String systemId) {
030        super(name);
031        Validate.notNull(publicId);
032        Validate.notNull(systemId);
033        attr(Name, name);
034        attr(PublicId, publicId);
035        attr(SystemId, systemId);
036        updatePubSyskey();
037    }
038
039    public void setPubSysKey(@Nullable String value) {
040        if (value != null)
041            attr(PubSysKey, value);
042    }
043
044    private void updatePubSyskey() {
045        if (has(PublicId)) {
046            attr(PubSysKey, PUBLIC_KEY);
047        } else if (has(SystemId))
048            attr(PubSysKey, SYSTEM_KEY);
049    }
050
051    /**
052     * Get this doctype's name (when set, or empty string)
053     * @return doctype name
054     */
055    public String name() {
056        return attr(Name);
057    }
058
059    /**
060     * Get this doctype's Public ID (when set, or empty string)
061     * @return doctype Public ID
062     */
063    public String publicId() {
064        return attr(PublicId);
065    }
066
067    /**
068     * Get this doctype's System ID (when set, or empty string)
069     * @return doctype System ID
070     */
071    public String systemId() {
072        return attr(SystemId);
073    }
074
075    @Override
076    public String nodeName() {
077        return Name;
078    }
079
080    @Override
081    void outerHtmlHead(Appendable accum, int depth, Document.OutputSettings out) throws IOException {
082        // add a newline if the doctype has a preceding node (which must be a comment)
083        if (siblingIndex > 0 && out.prettyPrint())
084            accum.append('\n');
085
086        if (out.syntax() == Syntax.html && !has(PublicId) && !has(SystemId)) {
087            // looks like a html5 doctype, go lowercase for aesthetics
088            accum.append("<!doctype");
089        } else {
090            accum.append("<!DOCTYPE");
091        }
092        if (has(Name))
093            accum.append(" ").append(attr(Name));
094        if (has(PubSysKey))
095            accum.append(" ").append(attr(PubSysKey));
096        if (has(PublicId))
097            accum.append(" \"").append(attr(PublicId)).append('"');
098        if (has(SystemId))
099            accum.append(" \"").append(attr(SystemId)).append('"');
100        accum.append('>');
101    }
102
103    @Override
104    void outerHtmlTail(Appendable accum, int depth, Document.OutputSettings out) {
105    }
106
107    private boolean has(final String attribute) {
108        return !StringUtil.isBlank(attr(attribute));
109    }
110}