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}