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