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}