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}