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 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    // 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        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    private void updatePubSyskey() {
046        if (has(PublicId)) {
047            attributes().add(PubSysKey, PUBLIC_KEY);
048        } else if (has(SystemId))
049            attributes().add(PubSysKey, SYSTEM_KEY);
050    }
051
052    /**
053     * Get this doctype's name (when set, or empty string)
054     * @return doctype name
055     */
056    public String name() {
057        return attr(NameKey);
058    }
059
060    /**
061     * Get this doctype's Public ID (when set, or empty string)
062     * @return doctype Public ID
063     */
064    public String publicId() {
065        return attr(PublicId);
066    }
067
068    /**
069     * Get this doctype's System ID (when set, or empty string)
070     * @return doctype System ID
071     */
072    public String systemId() {
073        return attr(SystemId);
074    }
075
076    @Override
077    public String nodeName() {
078        return "#doctype";
079    }
080
081    @Override
082    void outerHtmlHead(Appendable accum, Document.OutputSettings out) throws IOException {
083        if (out.syntax() == Syntax.html && !has(PublicId) && !has(SystemId)) {
084            // looks like a html5 doctype, go lowercase for aesthetics
085            accum.append("<!doctype");
086        } else {
087            accum.append("<!DOCTYPE");
088        }
089        if (has(NameKey))
090            accum.append(" ").append(attr(NameKey));
091        if (has(PubSysKey))
092            accum.append(" ").append(attr(PubSysKey));
093        if (has(PublicId))
094            accum.append(" \"").append(attr(PublicId)).append('"');
095        if (has(SystemId))
096            accum.append(" \"").append(attr(SystemId)).append('"');
097        accum.append('>');
098    }
099
100
101    private boolean has(final String attribute) {
102        return !StringUtil.isBlank(attr(attribute));
103    }
104}