001package org.jsoup.parser; 002 003import org.jsoup.helper.Validate; 004import org.jsoup.internal.SharedConstants; 005import org.jspecify.annotations.Nullable; 006 007import java.util.ArrayList; 008import java.util.HashMap; 009import java.util.Map; 010import java.util.Objects; 011import java.util.function.Consumer; 012 013import static org.jsoup.parser.Parser.NamespaceHtml; 014import static org.jsoup.parser.Parser.NamespaceMathml; 015import static org.jsoup.parser.Parser.NamespaceSvg; 016 017/** 018 A TagSet controls the {@link Tag} configuration for a Document's parse, and its serialization. It contains the initial 019 defaults, and after the parse, any additionally discovered tags. 020 021 @see Parser#tagSet(TagSet) 022 @since 1.20.1 023 */ 024public class TagSet { 025 static final TagSet HtmlTagSet = initHtmlDefault(); 026 027 private final Map<String, Map<String, Tag>> tags = new HashMap<>(); // namespace -> tag name -> Tag 028 private final @Nullable TagSet source; // internal fallback for lazy tag copies 029 private @Nullable ArrayList<Consumer<Tag>> customizers; // optional onNewTag tag customizer 030 031 /** 032 Returns a mutable copy of the default HTML tag set. 033 */ 034 public static TagSet Html() { 035 return new TagSet(HtmlTagSet, null); 036 } 037 038 private TagSet(@Nullable TagSet source, @Nullable ArrayList<Consumer<Tag>> customizers) { 039 this.source = source; 040 this.customizers = customizers; 041 } 042 043 public TagSet() { 044 this(null, null); 045 } 046 047 /** 048 Creates a new TagSet by copying the current tags and customizers from the provided source TagSet. Changes made to 049 one TagSet will not affect the other. 050 @param template the TagSet to copy 051 */ 052 public TagSet(TagSet template) { 053 this(template.source, copyCustomizers(template)); 054 // copy tags eagerly; any lazy pull-through should come only from the root source (which would be the HTML defaults), not the template itself. 055 // that way the template tagset is not mutated when we do read through 056 if (template.tags.isEmpty()) return; 057 058 for (Map.Entry<String, Map<String, Tag>> namespaceEntry : template.tags.entrySet()) { 059 Map<String, Tag> nsTags = new HashMap<>(namespaceEntry.getValue().size()); 060 for (Map.Entry<String, Tag> tagEntry : namespaceEntry.getValue().entrySet()) { 061 nsTags.put(tagEntry.getKey(), tagEntry.getValue().clone()); 062 } 063 tags.put(namespaceEntry.getKey(), nsTags); 064 } 065 } 066 067 private static @Nullable ArrayList<Consumer<Tag>> copyCustomizers(TagSet base) { 068 if (base.customizers == null) return null; 069 return new ArrayList<>(base.customizers); 070 } 071 072 /** 073 Insert a tag into this TagSet. If the tag already exists, it is replaced. 074 <p>Tags explicitly added like this are considered to be known tags (vs those that are dynamically created via 075 .valueOf() if not already in the set.</p> 076 077 @param tag the tag to add 078 @return this TagSet 079 */ 080 public TagSet add(Tag tag) { 081 tag.set(Tag.Known); 082 doAdd(tag); 083 return this; 084 } 085 086 /** Adds the tag, but does not set defined. Used in .valueOf */ 087 private void doAdd(Tag tag) { 088 if (customizers != null) { 089 for (Consumer<Tag> customizer : customizers) { 090 customizer.accept(tag); 091 } 092 } 093 tag.setParserOptions(); 094 095 tags.computeIfAbsent(tag.namespace, ns -> new HashMap<>()) 096 .put(tag.tagName, tag); 097 } 098 099 /** 100 Get an existing Tag from this TagSet by tagName and namespace. The tag name is not normalized, to support mixed 101 instances. 102 103 @param tagName the case-sensitive tag name 104 @param namespace the namespace 105 @return the tag, or null if not found 106 */ 107 public @Nullable Tag get(String tagName, String namespace) { 108 Validate.notNull(tagName); 109 Validate.notNull(namespace); 110 111 // get from our tags 112 Map<String, Tag> nsTags = tags.get(namespace); 113 if (nsTags != null) { 114 Tag tag = nsTags.get(tagName); 115 if (tag != null) { 116 return tag; 117 } 118 } 119 120 // not found; clone on demand from source if exists 121 if (source != null) { 122 Tag tag = source.get(tagName, namespace); 123 if (tag != null) { 124 Tag copy = tag.clone(); 125 doAdd(copy); 126 return copy; 127 } 128 } 129 130 return null; 131 } 132 133 /** 134 Tag.valueOf with the normalName via the token.normalName, to save redundant lower-casing passes. 135 Provide a null normalName unless we already have one; will be normalized if required from tagName. 136 */ 137 Tag valueOf(String tagName, @Nullable String normalName, String namespace, boolean preserveTagCase) { 138 Validate.notNull(tagName); 139 Validate.notNull(namespace); 140 if (normalName == null) tagName = tagName.trim(); // public API input; tokenizer names are already delimited 141 Validate.notEmpty(tagName); 142 Tag tag = get(tagName, namespace); 143 if (tag != null) return tag; 144 145 // not found by tagName, try by normal 146 if (normalName == null) normalName = ParseSettings.normalName(tagName); 147 tagName = preserveTagCase ? tagName : normalName; 148 tag = get(normalName, namespace); 149 if (tag != null) { 150 if (preserveTagCase && !tagName.equals(normalName)) { 151 tag = tag.clone(); // copy so that the name update doesn't reset all instances 152 tag.tagName = tagName; 153 doAdd(tag); 154 } 155 return tag; 156 } 157 158 // not defined: return a new one 159 tag = new Tag(tagName, normalName, namespace); 160 doAdd(tag); 161 162 return tag; 163 } 164 165 /** 166 Get a Tag by name from this TagSet. If not previously defined (unknown), returns a new tag. 167 <p>New tags will be added to this TagSet.</p> 168 169 @param tagName Name of tag, e.g. "p". 170 @param namespace the namespace for the tag. 171 @param settings used to control tag name sensitivity 172 @return The tag, either defined or new generic. 173 */ 174 public Tag valueOf(String tagName, String namespace, ParseSettings settings) { 175 return valueOf(tagName, null, namespace, settings.preserveTagCase()); 176 } 177 178 /** 179 Get a Tag by name from this TagSet. If not previously defined (unknown), returns a new tag. 180 <p>New tags will be added to this TagSet.</p> 181 182 @param tagName Name of tag, e.g. "p". <b>Case-sensitive</b>. 183 @param namespace the namespace for the tag. 184 @return The tag, either defined or new generic. 185 @see #valueOf(String tagName, String namespace, ParseSettings settings) 186 */ 187 public Tag valueOf(String tagName, String namespace) { 188 return valueOf(tagName, namespace, ParseSettings.preserveCase); 189 } 190 191 /** 192 Register a callback to customize each {@link Tag} as it's added to this TagSet. 193 <p>Customizers are invoked once per Tag, when they are added (explicitly or via the valueOf methods).</p> 194 195 <p>For example, to allow all unknown tags to be self-closing during when parsing as HTML:</p> 196 <pre><code> 197 Parser parser = Parser.htmlParser(); 198 parser.tagSet().onNewTag(tag -> { 199 if (!tag.isKnownTag()) 200 tag.set(Tag.SelfClose); 201 }); 202 203 Document doc = Jsoup.parse(html, parser); 204 </code></pre> 205 206 @param customizer a {@code Consumer<Tag>} that will be called for each newly added or cloned Tag; callers can 207 inspect and modify the Tag's state (e.g. set options) 208 @return this TagSet, to allow method chaining 209 @since 1.21.0 210 */ 211 public TagSet onNewTag(Consumer<Tag> customizer) { 212 Validate.notNull(customizer); 213 if (customizers == null) 214 customizers = new ArrayList<>(); 215 customizers.add(customizer); 216 return this; 217 } 218 219 @Override 220 public boolean equals(Object o) { 221 if (!(o instanceof TagSet)) return false; 222 TagSet tagSet = (TagSet) o; 223 return Objects.equals(tags, tagSet.tags); 224 } 225 226 @Override 227 public int hashCode() { 228 return Objects.hashCode(tags); 229 } 230 231 // Default HTML initialization 232 233 /** 234 Initialize the default HTML tag set. 235 */ 236 static TagSet initHtmlDefault() { 237 String[] blockTags = { 238 "html", "head", "body", "frameset", "script", "noscript", "style", "meta", "link", "title", "frame", 239 "noframes", "section", "nav", "aside", "hgroup", "header", "footer", "p", "h1", "h2", "h3", "h4", "h5", 240 "h6", "dialog", "search", 241 "ul", "ol", "pre", "div", "blockquote", "hr", "address", "figure", "figcaption", "form", "fieldset", 242 "dl", "dt", "dd", "li", "table", "caption", "thead", "tfoot", "tbody", "colgroup", "col", "tr", "th", 243 "td", "details", "menu", "plaintext", "template", "article", "main", 244 "center", 245 "dir", "applet", "marquee", "listing", // deprecated but still known / special handling 246 "#root" // the outer Document 247 }; 248 String[] inlineTags = { 249 "object", "base", "font", "tt", "i", "b", "u", "big", "small", "em", "strong", "dfn", "code", "samp", "kbd", 250 "var", "cite", "abbr", "time", "acronym", "mark", "ruby", "rt", "rp", "rtc", "a", "img", "wbr", "map", 251 "q", 252 "sub", "sup", "bdo", "iframe", "embed", "span", "input", "select", "textarea", "label", "audio", "video", "canvas", "optgroup", 253 "option", "legend", "datalist", "keygen", "output", "progress", "meter", "area", "param", "source", "track", 254 "summary", "command", "device", "basefont", "bgsound", "menuitem", "data", "bdi", "s", "strike", "nobr", 255 "ins", "del", "button", "picture", "slot", 256 "rb", // deprecated but still known / special handling 257 }; 258 String[] inlineContainers = { // pretty-print hint: block tags whose inline children should stay inline 259 "title", "p", "h1", "h2", "h3", "h4", "h5", "h6", "pre", "address", "li", "th", "td", "script", "style" 260 }; 261 String[] voidTags = { 262 "meta", "link", "base", "frame", "img", "br", "wbr", "embed", "hr", "input", "keygen", "col", "command", 263 "device", "area", "basefont", "bgsound", "menuitem", "param", "source", "track" 264 }; 265 String[] preserveWhitespaceTags = { 266 "pre", "plaintext", "title", "textarea", "script" 267 }; 268 String[] rcdataTags = { "title", "textarea" }; 269 String[] dataTags = { "iframe", "noembed", "noframes", "script", "style", "xmp" }; 270 String[] formSubmitTags = SharedConstants.FormSubmitTags; 271 String[] textBoundaryTags = { // text() readability hint for controls, widgets, and embedded objects 272 "button", "input", "select", "textarea", "option", "output", "progress", "meter", 273 "img", "picture", "audio", "video", "canvas", "object", "embed", "iframe" 274 }; 275 String[] blockMathTags = {"math"}; 276 String[] inlineMathTags = {"mi", "mo", "msup", "mn", "mtext"}; 277 String[] blockSvgTags = {"svg", "femerge", "femergenode"}; // note these are LC versions, but actually preserve case 278 String[] inlineSvgTags = {"text"}; 279 String[] dataSvgTags = {"script"}; 280 281 return new TagSet() 282 .setupTags(NamespaceHtml, blockTags, tag -> tag.set(Tag.Block)) 283 .setupTags(NamespaceHtml, inlineTags, tag -> tag.set(0)) 284 .setupTags(NamespaceHtml, inlineContainers, tag -> tag.set(Tag.InlineContainer)) 285 .setupTags(NamespaceHtml, voidTags, tag -> tag.set(Tag.Void)) 286 .setupTags(NamespaceHtml, preserveWhitespaceTags, tag -> tag.set(Tag.PreserveWhitespace)) 287 .setupTags(NamespaceHtml, rcdataTags, tag -> tag.set(Tag.RcData)) 288 .setupTags(NamespaceHtml, dataTags, tag -> tag.set(Tag.Data)) 289 .setupTags(NamespaceHtml, formSubmitTags, tag -> tag.set(Tag.FormSubmittable)) 290 .setupTags(NamespaceHtml, textBoundaryTags, tag -> tag.set(Tag.TextBoundary)) 291 .setupTags(NamespaceMathml, blockMathTags, tag -> tag.set(Tag.Block)) 292 .setupTags(NamespaceMathml, inlineMathTags, tag -> tag.set(0)) 293 .setupTags(NamespaceSvg, blockSvgTags, tag -> tag.set(Tag.Block)) 294 .setupTags(NamespaceSvg, inlineSvgTags, tag -> tag.set(0)) 295 .setupTags(NamespaceSvg, dataSvgTags, tag -> tag.set(Tag.Data)) 296 ; 297 } 298 299 private TagSet setupTags(String namespace, String[] tagNames, Consumer<Tag> tagModifier) { 300 for (String tagName : tagNames) { 301 Tag tag = get(tagName, namespace); 302 if (tag == null) { 303 tag = new Tag(tagName, tagName, namespace); // normal name is already normal here 304 tag.options = 0; // clear defaults 305 add(tag); 306 } 307 tagModifier.accept(tag); 308 } 309 return this; 310 } 311}