001package org.jsoup.nodes;
002
003import org.jsoup.internal.QuietAppendable;
004import org.jsoup.internal.StringUtil;
005import org.jsoup.helper.Validate;
006import org.jsoup.nodes.Document.OutputSettings.Syntax;
007import org.jspecify.annotations.Nullable;
008
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 NameKey = "name";
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    private static final String InternalSubsetKey = Attributes.internalKey("doctypeInternalSubset");
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        attributes()
034            .add(NameKey, name)
035            .add(PublicId, publicId)
036            .add(SystemId, systemId);
037        updatePubSyskey();
038    }
039
040    public void setPubSysKey(@Nullable String value) {
041        if (value != null)
042            attr(PubSysKey, value);
043    }
044
045    /**
046     Sets the raw XML internal subset for serialization.
047     @param value the internal subset contents
048     */
049    public void setInternalSubset(String value) {
050        attributes().put(InternalSubsetKey, value);
051    }
052
053    private void updatePubSyskey() {
054        if (has(PublicId)) {
055            attributes().add(PubSysKey, PUBLIC_KEY);
056        } else if (has(SystemId))
057            attributes().add(PubSysKey, SYSTEM_KEY);
058    }
059
060    /**
061     * Get this doctype's name (when set, or empty string)
062     * @return doctype name
063     */
064    public String name() {
065        return attr(NameKey);
066    }
067
068    /**
069     * Get this doctype's Public ID (when set, or empty string)
070     * @return doctype Public ID
071     */
072    public String publicId() {
073        return attr(PublicId);
074    }
075
076    /**
077     * Get this doctype's System ID (when set, or empty string)
078     * @return doctype System ID
079     */
080    public String systemId() {
081        return attr(SystemId);
082    }
083
084    @Override
085    public String nodeName() {
086        return "#doctype";
087    }
088
089    @Override
090    void outerHtmlHead(QuietAppendable accum, Document.OutputSettings out) {
091        if (out.syntax() == Syntax.html && !has(PublicId) && !has(SystemId)) {
092            // looks like a html5 doctype, go lowercase for aesthetics
093            accum.append("<!doctype");
094        } else {
095            accum.append("<!DOCTYPE");
096        }
097        if (has(NameKey))
098            accum.append(" ").append(attr(NameKey));
099        if (has(PubSysKey))
100            accum.append(" ").append(attr(PubSysKey));
101        if (has(PublicId))
102            accum.append(" \"").append(attr(PublicId)).append('"');
103        if (has(SystemId))
104            accum.append(" \"").append(attr(SystemId)).append('"');
105        if (attributes().hasKey(InternalSubsetKey)) // only if via the xml parser; html parser will drop
106            accum.append(" [").append(attr(InternalSubsetKey)).append(']');
107        accum.append('>');
108    }
109
110
111    private boolean has(final String attribute) {
112        return !StringUtil.isBlank(attr(attribute));
113    }
114}