001package org.jsoup.parser;
002
003import org.jsoup.helper.Validate;
004import org.jsoup.internal.Normalizer;
005import org.jsoup.internal.StringUtil;
006import org.jsoup.nodes.Attributes;
007import org.jsoup.nodes.CDataNode;
008import org.jsoup.nodes.Comment;
009import org.jsoup.nodes.DataNode;
010import org.jsoup.nodes.Document;
011import org.jsoup.nodes.Element;
012import org.jsoup.nodes.FormElement;
013import org.jsoup.nodes.Node;
014import org.jsoup.nodes.NodeInternals;
015import org.jsoup.nodes.ProcessingInstruction;
016import org.jsoup.nodes.TextNode;
017import org.jspecify.annotations.Nullable;
018
019import java.io.Reader;
020import java.util.ArrayList;
021import java.util.Collection;
022import java.util.List;
023
024import static org.jsoup.internal.StringUtil.inSorted;
025import static org.jsoup.parser.HtmlTreeBuilderState.Constants.Headings;
026import static org.jsoup.parser.HtmlTreeBuilderState.Constants.InTableFoster;
027import static org.jsoup.parser.HtmlTreeBuilderState.ForeignContent;
028import static org.jsoup.parser.Parser.*;
029
030/**
031 * HTML Tree Builder; creates a DOM from Tokens.
032 */
033public class HtmlTreeBuilder extends TreeBuilder {
034    static final String[] TagMathMlTextIntegration = new String[]{"mi", "mn", "mo", "ms", "mtext"};
035    static final String[] TagSvgHtmlIntegration = new String[]{"desc", "foreignObject", "title"};
036    static final String[] TagFormListed = {
037        "button", "fieldset", "input", "keygen", "object", "output", "select", "textarea"
038    };
039
040    private HtmlTreeBuilderState state; // the current state
041    private HtmlTreeBuilderState originalState; // original / marked state
042
043    private boolean baseUriSetFromDoc;
044    private @Nullable Element headElement; // the current head element
045    private @Nullable FormElement formElement; // the current form element
046    private @Nullable Element contextElement; // context copy and fragment output container; not on the stack
047    private @Nullable Element fragmentRoot; // internal stack root; contextElement receives fragment nodes
048    ArrayList<@Nullable Element> formattingElements; // active formatting elements and null scope markers
049    private ArrayList<HtmlTreeBuilderState> tmplInsertMode; // stack of Template Insertion modes
050    private @Nullable NoscriptState noscriptState; // active noscript island state
051    private List<Token.Character> pendingTableCharacters; // chars in table to be shifted out
052    private Token.EndTag emptyEnd; // reused empty end tag
053
054    private boolean framesetOk; // if ok to go into frameset
055    private boolean fosterInserts; // if next inserts should be fostered
056    private boolean fragmentParsing; // if parsing a fragment of html
057
058    @Override ParseSettings defaultSettings() {
059        return ParseSettings.htmlDefault;
060    }
061
062    @Override
063    HtmlTreeBuilder newInstance() {
064        return new HtmlTreeBuilder();
065    }
066
067    @Override
068    protected void initialiseParse(Reader input, String baseUri, Parser parser) {
069        super.initialiseParse(input, baseUri, parser);
070
071        // this is a bit mucky. todo - probably just create new parser objects to ensure all reset.
072        state = HtmlTreeBuilderState.Initial;
073        originalState = null;
074        baseUriSetFromDoc = false;
075        headElement = null;
076        formElement = null;
077        contextElement = null;
078        fragmentRoot = null;
079        formattingElements = new ArrayList<>();
080        tmplInsertMode = new ArrayList<>();
081        noscriptState = null;
082        pendingTableCharacters = new ArrayList<>();
083        emptyEnd = new Token.EndTag(this);
084        framesetOk = true;
085        fosterInserts = false;
086        fragmentParsing = false;
087    }
088
089    @Override void initialiseParseFragment(@Nullable Element context) {
090        // context may be null
091        state = HtmlTreeBuilderState.Initial;
092        fragmentParsing = true;
093
094        if (context != null) {
095            final String contextName = context.normalName();
096            contextElement = new Element(context.tag(), baseUri);
097            contextElement.attributes().addAll(context.attributes());
098            if (context.ownerDocument() != null) // quirks setup:
099                doc.quirksMode(context.ownerDocument().quirksMode());
100
101            // initialise the tokeniser state
102            Tag contextTag = contextElement.tag();
103            boolean htmlContext = NamespaceHtml.equals(contextTag.namespace());
104            TokeniserState contextState = contextTag.textState(); // style, xmp, title, textarea, etc; or custom
105            if (contextState == null) contextState = TokeniserState.Data;
106
107            switch (contextName) {
108                case "script":
109                    if (htmlContext) {
110                        contextState = TokeniserState.ScriptData;
111                    } else if (NamespaceSvg.equals(contextTag.namespace())) {
112                        // svg script enters script data during document parsing, but fragments start in data so markup creates svg children
113                        contextState = TokeniserState.Data;
114                    }
115                    break;
116                case "plaintext":
117                    if (htmlContext) contextState = TokeniserState.PLAINTEXT;
118                    break;
119                case "template":
120                    if (htmlContext) {
121                        contextState = TokeniserState.Data;
122                        pushTemplateMode(HtmlTreeBuilderState.InTemplate);
123                    }
124                    break;
125            }
126            tokeniser.transition(contextState);
127            fragmentRoot = new Element(tagFor("html", "html", NamespaceHtml, settings), baseUri);
128            doc.appendChild(contextElement);
129            push(fragmentRoot);
130            resetInsertionMode();
131
132            // setup form element to nearest form on context (up ancestor chain). ensures form controls are associated
133            // with form correctly
134            Element formSearch = context;
135            while (formSearch != null) {
136                if (formSearch instanceof FormElement) {
137                    formElement = (FormElement) formSearch;
138                    break;
139                }
140                formSearch = formSearch.parent();
141            }
142
143            if (htmlContext && contextName.equals("noscript")) enterNoscript(fragmentRoot);
144        }
145    }
146
147    @Override List<Node> completeParseFragment() {
148        return (contextElement != null ? contextElement : doc).childNodes();
149    }
150
151    /**
152     Reads a token, separating leading whitespace in insertion modes that handle it separately.
153     */
154    @Override
155    Token readToken() {
156        // the spec processes Data characters individually; return leading whitespace first
157        // so that the current insertion mode handles it, before following text takes another route
158        if (noscriptState == null && useCurrentOrForeignInsert(tokeniser.charPending)) {
159            switch (state) {
160                case Initial:
161                case BeforeHtml:
162                case BeforeHead:
163                case InHead:
164                case AfterHead:
165                case InColumnGroup:
166                case AfterBody:
167                case AfterAfterBody:
168                case AfterAfterFrameset:
169                    return tokeniser.readWhitespace();
170            }
171        }
172        return tokeniser.read();
173    }
174
175    @Override
176    protected boolean process(Token token) {
177        if (noscriptState != null && state != HtmlTreeBuilderState.Text)
178            return processNoscriptToken(token);
179        HtmlTreeBuilderState dispatch = useCurrentOrForeignInsert(token) ? this.state : ForeignContent;
180        return dispatch.process(token, this);
181    }
182
183    /**
184     Handles tokens in a noscript island as plain contained markup. This diverges from the spec intentionally so that
185     content is available in the DOM and round-trip serializable, but errant content won't change the parser's context
186     (e.g. an `a` won't kick out of InHead).
187     */
188    private boolean processNoscriptToken(Token token) {
189        switch (token.type) {
190            case StartTag:
191                return insertNoscriptStartTag(token.asStartTag());
192            case EndTag:
193                return closeNoscriptEndTag(token.asEndTag());
194            case Comment:
195                insertCommentNode(token.asComment());
196                return true;
197            case Character:
198                Token.Character character = token.asCharacter();
199                insertCharacterNode(character);
200                if (!StringUtil.isBlank(character.getData()))
201                    framesetOk(false);
202                return true;
203            case Doctype:
204                error(state);
205                return false;
206            case EOF:
207                error(state);
208                endNoscript();
209                return process(token);
210            default:
211                Validate.wtf("Unexpected state: " + token.type); // XmlDecl only in XmlTreeBuilder
212                return false;
213        }
214    }
215
216    /**
217     Inserts a start tag inside a noscript island as plain markup.
218     */
219    private boolean insertNoscriptStartTag(Token.StartTag start) {
220        TokeniserState textState = tagFor(start).textState();
221        Element el = insertElementFor(start);
222        if (textState != null) { // plaintext is intentionally not TagSet-driven and remains plain fallback markup.
223            if (start.isSelfClosing()) {
224                if (currentElement() == el)
225                    pop();
226            } else {
227                tokeniser.transition(textState);
228                markInsertionMode();
229                transition(HtmlTreeBuilderState.Text);
230            }
231        }
232
233        framesetOk(false);
234        return true;
235    }
236
237    /**
238     Closes an island element if it matches above the current noscript boundary.
239     */
240    private boolean closeNoscriptEndTag(Token.EndTag end) {
241        String name = end.normalName();
242        NoscriptState island = Validate.expectNotNull(noscriptState, "Bug: noscript end tag processed with no island state");
243        if (name.equals("noscript") && island.boundary != fragmentRoot) {
244            endNoscript();
245            return true;
246        }
247        if (!inNoscriptScope(name)) {
248            error(state);
249            return false;
250        }
251        if (!currentElementIs(name))
252            error(state);
253        popStackToClose(name);
254        return true;
255    }
256
257    boolean useCurrentOrForeignInsert(Token token) {
258        // https://html.spec.whatwg.org/multipage/parsing.html#tree-construction
259        // If the stack of open elements is empty
260        if (stack.isEmpty())
261            return true;
262        final Element el = adjustedCurrentElement();
263        final String ns = el.tag().namespace();
264
265        // If the adjusted current node is an element in the HTML namespace
266        if (NamespaceHtml.equals(ns))
267            return true;
268
269        // If the adjusted current node is a MathML text integration point and the token is a start tag whose tag name is neither "mglyph" nor "malignmark"
270        // If the adjusted current node is a MathML text integration point and the token is a character token
271        if (isMathmlTextIntegration(el)) {
272            if (token.isStartTag()
273                    && !"mglyph".equals(token.asStartTag().normalName)
274                    && !"malignmark".equals(token.asStartTag().normalName))
275                    return true;
276            if (token.isCharacter())
277                    return true;
278        }
279        // If the adjusted current node is a MathML annotation-xml element and the token is a start tag whose tag name is "svg"
280        if (Parser.NamespaceMathml.equals(ns)
281            && el.nameIs("annotation-xml")
282            && token.isStartTag()
283            && "svg".equals(token.asStartTag().normalName))
284            return true;
285
286        // If the adjusted current node is an HTML integration point and the token is a start tag
287        // If the adjusted current node is an HTML integration point and the token is a character token
288        if (isHtmlIntegration(el)
289            && (token.isStartTag() || token.isCharacter()))
290            return true;
291
292        // If the token is an end-of-file token
293        return token.isEOF();
294    }
295
296    static boolean isMathmlTextIntegration(Element el) {
297        /*
298        A node is a MathML text integration point if it is one of the following elements:
299        A MathML mi element
300        A MathML mo element
301        A MathML mn element
302        A MathML ms element
303        A MathML mtext element
304         */
305        return (Parser.NamespaceMathml.equals(el.tag().namespace())
306            && StringUtil.inSorted(el.normalName(), TagMathMlTextIntegration));
307    }
308
309    /** Tests if an element is in the HTML namespace. */
310    static boolean isHtmlEl(Element el) {
311        return NamespaceHtml.equals(el.tag().namespace());
312    }
313
314    /** Tests if an HTML element has the supplied normal name. */
315    static boolean isHtmlEl(Element el, String normalName) {
316        return isHtmlEl(el) && el.normalName().equals(normalName);
317    }
318
319    /** Tests if an HTML element has one of the supplied sorted normal names. */
320    static boolean isHtmlEl(Element el, String[] normalNames) {
321        return isHtmlEl(el) && inSorted(el.normalName(), normalNames);
322    }
323
324    static boolean isHtmlIntegration(Element el) {
325        /*
326        A node is an HTML integration point if it is one of the following elements:
327        A MathML annotation-xml element whose start tag token had an attribute with the name "encoding" whose value was an ASCII case-insensitive match for the string "text/html"
328        A MathML annotation-xml element whose start tag token had an attribute with the name "encoding" whose value was an ASCII case-insensitive match for the string "application/xhtml+xml"
329        An SVG foreignObject element
330        An SVG desc element
331        An SVG title element
332         */
333        if (Parser.NamespaceMathml.equals(el.tag().namespace())
334            && el.nameIs("annotation-xml")) {
335            String encoding = Normalizer.asciiLowerCase(el.attr("encoding"));
336            if (encoding.equals("text/html") || encoding.equals("application/xhtml+xml"))
337                return true;
338        }
339        // note using .tagName for case-sensitive hit here of foreignObject
340        return Parser.NamespaceSvg.equals(el.tag().namespace()) && StringUtil.in(el.tagName(), TagSvgHtmlIntegration);
341    }
342
343    boolean process(Token token, HtmlTreeBuilderState state) {
344        return state.process(token, this);
345    }
346
347    void transition(HtmlTreeBuilderState state) {
348        this.state = state;
349    }
350
351    HtmlTreeBuilderState state() {
352        return state;
353    }
354
355    void markInsertionMode() {
356        originalState = state;
357    }
358
359    HtmlTreeBuilderState originalState() {
360        return originalState;
361    }
362
363    void framesetOk(boolean framesetOk) {
364        this.framesetOk = framesetOk;
365    }
366
367    boolean framesetOk() {
368        return framesetOk;
369    }
370
371    Document getDocument() {
372        return doc;
373    }
374
375    String getBaseUri() {
376        return baseUri;
377    }
378
379    void maybeSetBaseUri(Element base) {
380        if (baseUriSetFromDoc) // only listen to the first <base href> in parse
381            return;
382
383        String href = base.absUrl("href");
384        if (href.length() != 0) { // ignore <base target> etc
385            baseUri = href;
386            baseUriSetFromDoc = true;
387            doc.setBaseUri(href); // set on the doc so doc.createElement(Tag) will get updated base, and to update all descendants
388        }
389    }
390
391    boolean isFragmentParsing() {
392        return fragmentParsing;
393    }
394
395    /** Tests whether this fragment was created with the named HTML context element. */
396    boolean fragmentContextIs(String normalName) {
397        return fragmentParsing && contextElement != null && isHtmlEl(contextElement, normalName);
398    }
399
400    /** Tests whether the parser is inside a template or using a template fragment context. */
401    boolean isParsingTemplateContents() {
402        return onStack("template") || fragmentContextIs("template");
403    }
404
405    void error(HtmlTreeBuilderState state) {
406        if (parser.getErrors().canAddError())
407            parser.getErrors().add(new ParseError(reader, "Unexpected %s token [%s] when in state [%s]",
408                currentToken.tokenType(), currentToken, state));
409    }
410
411    /** Creates an element after normalizing its attributes for the selected namespace. */
412    Element createElementFor(Token.StartTag startTag, String namespace) {
413        // normalize before deduping so differently cased names resolve to one attribute
414        Attributes attributes = startTag.attributes;
415        boolean isHtml = NamespaceHtml.equals(namespace);
416        if (attributes != null && !attributes.isEmpty()) {
417            if (isHtml)
418                settings.normalizeAttributes(attributes);
419            else if (!settings.preserveAttributeCase())
420                ForeignNames.normalizeAttributes(attributes, namespace);
421            int dupes = attributes.deduplicate(settings);
422            if (dupes > 0)
423                error("Dropped duplicate attribute(s) in tag [%s]", startTag.normalName);
424            startTag.finaliseAttributeRanges(settings);
425        }
426
427        Tag tag = tagFor(startTag.name(), startTag.normalName, namespace, isHtml ? settings : ParseSettings.preserveCase);
428        if (isHtml && tag.normalName().equals("form"))
429            return new FormElement(tag, null, attributes);
430        return new Element(tag, null, attributes);
431    }
432
433    /** Inserts an HTML element for the given tag */
434    Element insertElementFor(final Token.StartTag startTag) {
435        Element el = createElementFor(startTag, NamespaceHtml);
436        doInsertElement(el);
437
438        // handle self-closing tags. when the spec expects an empty (void) tag, will directly hit insertEmpty, so won't generate this fake end tag.
439        if (startTag.isSelfClosing()) {
440            Tag tag = el.tag();
441            tag.setSeenSelfClose(); // can infer output if in xml syntax
442            if (tag.isEmpty()) {
443                // treated as empty below; nothing further
444            } else if (tag.isKnownTag() && tag.isSelfClosing()) {
445                // ok, allow it. effectively a pop, but fiddles with the state. handles empty style, title etc which would otherwise leave us in data state
446                tokeniser.transition(TokeniserState.Data); // handles <script />, otherwise needs breakout steps from script data
447                tokeniser.emit(emptyEnd.reset().name(el.tagName()));  // ensure we get out of whatever state we are in. emitted for yielded processing
448            } else {
449                // error it, and leave the inserted element on
450                tokeniser.error("Tag [%s] cannot be self-closing; not a void tag", tag.normalName());
451            }
452        }
453
454        if (el.tag().isEmpty()) {
455            pop(); // custom void tags behave like built-in voids (no children, not left on the stack); known empty go via insertEmpty
456        }
457
458        return el;
459    }
460
461    /** Inserts a foreign element with its configured SVG or MathML name adjustments. */
462    Element insertForeignElementFor(final Token.StartTag startTag, String namespace) {
463        if (!settings.preserveTagCase())
464            startTag.name(ForeignNames.tagName(startTag.name(), namespace));
465        Element el = createElementFor(startTag, namespace);
466        doInsertElement(el);
467
468        if (startTag.isSelfClosing()) { // foreign els are OK to self-close
469            el.tag().setSeenSelfClose(); // remember this is self-closing for output
470            pop();
471        }
472
473        return el;
474    }
475
476    Element insertEmptyElementFor(Token.StartTag startTag) {
477        Element el = createElementFor(startTag, NamespaceHtml);
478        doInsertElement(el);
479        pop();
480        return el;
481    }
482
483    /** Inserts a form and leaves the form pointer unset while parsing template contents. */
484    FormElement insertFormElement(Token.StartTag startTag, boolean onStack) {
485        FormElement el = (FormElement) createElementFor(startTag, NamespaceHtml);
486        if (!isParsingTemplateContents()) setFormElement(el);
487
488        doInsertElement(el);
489        if (!onStack) pop();
490        return el;
491    }
492
493    /** Inserts the Element onto the stack. All element inserts must run through this method. Performs any general
494     tests on the Element before insertion.
495     * @param el the Element to insert and make the current element
496     */
497    private void doInsertElement(Element el) {
498        enforceStackDepthLimit();
499
500        if (formElement != null && isHtmlEl(el, TagFormListed))
501            formElement.addElement(el); // connect form controls to their form element
502
503        // in HTML, the xmlns attribute if set must match what the parser set the tag's namespace to
504        if (parser.getErrors().canAddError() && el.hasAttr("xmlns") && !el.attr("xmlns").equals(el.tag().namespace()))
505            error("Invalid xmlns attribute [%s] on tag [%s]", el.attr("xmlns"), el.tagName());
506
507        insertNode(el, currentElOrDoc());
508        push(el);
509    }
510
511    /** Inserts a node into the target, applying foster parenting when required. */
512    void insertNode(Node node, Element target) {
513        if (fosterInserts && isHtmlEl(target, InTableFoster))
514            insertInFosterParent(node);
515        else
516            insertionTarget(target).appendChild(node);
517    }
518
519    /** Inserts a node at the foster parenting location. */
520    void insertInFosterParent(Node node) {
521        for (int pos = stack.size() - 1; pos >= 0; pos--) {
522            Element el = stack.get(pos);
523            if (isHtmlEl(el, "template")) {
524                el.appendChild(node); // template contents are stored on the element
525                return;
526            }
527            if (isHtmlEl(el, "table")) {
528                Element parent = el.parent();
529                if (parent != null)
530                    el.before(node);
531                else
532                    insertionTarget(stack.get(pos - 1)).appendChild(node);
533                return;
534            }
535        }
536        // https://html.spec.whatwg.org/multipage/parsing.html#appropriate-place-for-inserting-a-node
537        // fragment case: insert into the first element on the stack (the html element)
538        insertionTarget(stack.get(0)).appendChild(node);
539    }
540
541    /** Moves a node for adoption agency recovery to its adjusted insertion location. */
542    void insertAdopted(Node node, Element target) {
543        if (fosterInserts && isHtmlEl(target, InTableFoster)) {
544            for (int pos = stack.size() - 1; pos >= 0; pos--) {
545                Element el = stack.get(pos);
546                if (isHtmlEl(el, "template")) {
547                    insertAdopted(node, el, null); // template contents are stored on the element
548                    return;
549                }
550                if (isHtmlEl(el, "table")) {
551                    Element parent = el.parent();
552                    if (parent != null)
553                        insertAdopted(node, parent, el);
554                    else
555                        insertAdopted(node, insertionTarget(stack.get(pos - 1)), null);
556                    return;
557                }
558            }
559            target = stack.get(0);
560        }
561        insertAdopted(node, insertionTarget(target), null);
562    }
563
564    /**
565     Moves a node to a resolved parent and position, subject to the DOM insertion validity rules.
566     @param node the node to remove and reinsert
567     @param parent the element to insert the node into
568     @param before a child of {@code parent} to insert the node before, or {@code null} to append the node
569     */
570    static void insertAdopted(Node node, Element parent, @Nullable Node before) {
571        // https://html.spec.whatwg.org/multipage/parsing.html#adoption-agency-algorithm
572        // 4.15: If lastNode's parent is non-null, then remove lastNode.
573        if (node.parent() != null) node.remove();
574        // 4.16: Insert lastNode into target before refNode only if pre-insert validity holds.
575        if (node.parent() != null || (before != null && before.parent() != parent)) return;
576        if (parent instanceof Document && parent.childrenSize() != 0) return;
577        for (Node ancestor = parent; ancestor != null; ancestor = ancestor.parent()) {
578            if (ancestor == node) return;
579        }
580        if (before == null)
581            parent.appendChild(node);
582        else
583            before.before(node);
584    }
585
586    /** Inserts a comment into the current element, or the document when there is none. */
587    void insertCommentNode(Token.Comment token) {
588        insertCommentNode(token, currentElOrDoc());
589    }
590
591    /** Inserts a comment into the supplied target. */
592    void insertCommentNode(Token.Comment token, Element target) {
593        Node node;
594        if (token.isPI()) {
595            Token.PI piToken = token.asPI();
596            ProcessingInstruction instruction = new ProcessingInstruction(piToken.target(), token.getData());
597            NodeInternals.sourceDataStart(instruction, piToken.dataStartPos);
598            node = instruction;
599        } else {
600            node = new Comment(token.getData());
601        }
602        insertionTarget(target).appendChild(node);
603        onNodeInserted(node);
604    }
605
606    /** Inserts the provided character token into the current element, or the document when there is none. */
607    void insertCharacterNode(Token.Character characterToken) {
608        insertCharacterNode(characterToken, false);
609    }
610
611    /**
612     Inserts the provided character token into the current element, or the document when there is none.
613     The tokenizer will have already raised precise character errors.
614
615     @param characterToken the character token to insert
616     @param replace if true, replaces any null chars in the data with the replacement char (U+FFFD). If false, removes
617     null chars.
618     */
619    void insertCharacterNode(Token.Character characterToken, boolean replace) {
620        characterToken.normalizeNulls(replace);
621        if (characterToken.getData().isEmpty()) return;
622        Element target = currentElOrDoc();
623        Node node = createCharacterNode(characterToken, insertionTarget(target));
624        insertNode(node, target);
625        onNodeInserted(node);
626    }
627
628    /** Inserts the provided character token into the provided element. */
629    void insertCharacterToElement(Token.Character characterToken, Element el) {
630        Element target = insertionTarget(el);
631        Node node = createCharacterNode(characterToken, target);
632        target.appendChild(node); // insert at the requested element, without foster parenting
633        onNodeInserted(node);
634    }
635
636    /** Creates the text or data node represented by a character token. */
637    private Node createCharacterNode(Token.Character characterToken, Element target) {
638        final String data = characterToken.getData();
639
640        if (characterToken.isCData())
641            return new CDataNode(data);
642        else if (target.tag().is(Tag.Data))
643            return new DataNode(data);
644        else
645            return new TextNode(data);
646    }
647
648    ArrayList<Element> getStack() {
649        return stack;
650    }
651
652    /** Gets the insertion target for fragment content. */
653    private Element insertionTarget(Element target) {
654        // the html root is only used by the tree builder; content belongs in the context copy
655        return target == fragmentRoot && contextElement != null ? contextElement : target;
656    }
657
658    /** Notifies listeners that a node was inserted. */
659    @Override void onNodeInserted(Node node) {
660        // listeners observe the fragment's context copy, not its parser-only html root
661        super.onNodeInserted(node == fragmentRoot && contextElement != null ? contextElement : node);
662    }
663
664    /** Notifies listeners that a node was closed. */
665    @Override void onNodeClosed(Node node) {
666        super.onNodeClosed(node == fragmentRoot && contextElement != null ? contextElement : node);
667    }
668
669    /** Tests whether an element is open. */
670    @Override boolean isOpen(Element element) {
671        // the context copy remains open for streaming while its parser root is open
672        return super.isOpen(element == contextElement && fragmentRoot != null ? fragmentRoot : element);
673    }
674
675    /** Copies the open elements. */
676    @Override void copyOpenElementsTo(Collection<? super Element> elements) {
677        // streaming tracks the context copy in place of the parser-only root
678        for (Element element : stack)
679            elements.add(insertionTarget(element));
680    }
681
682    boolean onStack(Element el) {
683        return stack.contains(el);
684    }
685
686    /** Checks if there is an HTML element with the given name on the stack. */
687    boolean onStack(String elName) {
688        return getFromStack(elName) != null;
689    }
690
691    /** Gets the adjusted current element for foreign-content dispatch. */
692    private Element adjustedCurrentElement() {
693        // fragment parsing uses the context at the parser root
694        return fragmentParsing && stack.size() == 1 && contextElement != null ? contextElement : currentElement();
695    }
696
697    /** Gets the namespace used to process the current token. */
698    @Override String currentElNs() {
699        return adjustedCurrentElement().tag().namespace();
700    }
701
702    /** Gets the nearest (lowest) HTML element with the given name from the stack. */
703    @Nullable
704    Element getFromStack(String elName) {
705        for (int pos = stack.size() - 1; pos >= 0; pos--) {
706            Element next = stack.get(pos);
707            if (isHtmlEl(next, elName)) {
708                return next;
709            }
710        }
711        return null;
712    }
713
714    boolean removeFromStack(Element el) {
715        for (int pos = stack.size() -1; pos >= 0; pos--) {
716            Element next = stack.get(pos);
717            if (next == el) {
718                stack.remove(pos);
719                onNodeClosed(el);
720                return true;
721            }
722        }
723        return false;
724    }
725
726    @Override
727    void onStackPrunedForDepth(Element element) {
728        // handle other effects of popping to keep state correct
729        if (element == headElement) headElement = null;
730        if (element == formElement) setFormElement(null);
731        removeFromActiveFormattingElements(element);
732        if (element.nameIs("template")) {
733            clearFormattingElementsToLastMarker();
734            if (templateModeSize() > 0)
735                popTemplateMode();
736            resetInsertionMode();
737        } else if (noscriptState != null && element == noscriptState.boundary) {
738            restoreNoscriptState();
739        }
740    }
741
742    /** Pops the stack until the given HTML element is removed. */
743    @Nullable
744    Element popStackToClose(String elName) {
745        for (int pos = stack.size() -1; pos >= 0; pos--) {
746            Element el = pop();
747            if (isHtmlEl(el, elName)) {
748                return el;
749            }
750        }
751        return null;
752    }
753
754    /** Pops the stack until an element with the supplied name is removed, irrespective of namespace. */
755    @Nullable
756    Element popStackToCloseAnyNamespace(String elName) {
757        for (int pos = stack.size() -1; pos >= 0; pos--) {
758            Element el = pop();
759            if (el.nameIs(elName)) {
760                return el;
761            }
762        }
763        return null;
764    }
765
766    /** Pops the stack until one of the given HTML elements is removed. */
767    void popStackToClose(String... elNames) { // elnames is sorted, comes from Constants
768        for (int pos = stack.size() -1; pos >= 0; pos--) {
769            Element el = pop();
770            if (isHtmlEl(el, elNames)) {
771                break;
772            }
773        }
774    }
775
776    void clearStackToTableContext() {
777        clearStackToContext("table", "template");
778    }
779
780    void clearStackToTableBodyContext() {
781        clearStackToContext("tbody", "tfoot", "thead", "template");
782    }
783
784    void clearStackToTableRowContext() {
785        clearStackToContext("tr", "template");
786    }
787
788    /** Removes elements from the stack until one of the supplied HTML elements is removed. */
789    private void clearStackToContext(String... nodeNames) {
790        for (int pos = stack.size() -1; pos >= 0; pos--) {
791            Element next = stack.get(pos);
792            if (isHtmlEl(next) &&
793                (StringUtil.in(next.normalName(), nodeNames) || next.nameIs("html")))
794                break;
795            else
796                pop();
797        }
798    }
799
800    /**
801     Gets the Element immediately above the supplied element on the stack. Which due to adoption, may not necessarily be
802     its parent.
803
804     @param el
805     @return the Element immediately above the supplied element, or null if there is no such element.
806     */
807    @Nullable Element aboveOnStack(Element el) {
808        if (!onStack(el)) return null;
809        for (int pos = stack.size() -1; pos > 0; pos--) {
810            Element next = stack.get(pos);
811            if (next == el) {
812                return stack.get(pos-1);
813            }
814        }
815        return null;
816    }
817
818    /** Inserts an element immediately after an existing stack entry. */
819    void insertOnStackAfter(Element after, Element in) {
820        int i = stack.lastIndexOf(after);
821        if (i == -1) {
822            error("Unable to place <%s> after <%s> while recovering misnested formatting", in.tagName(), after.tagName());
823            stack.add(in);
824        } else {
825            stack.add(i + 1, in);
826        }
827    }
828
829    /** Replaces an existing element without changing any scope markers. */
830    private static void replaceInQueue(ArrayList<@Nullable Element> queue, Element out, Element in) {
831        int i = queue.lastIndexOf(out);
832        Validate.isTrue(i != -1);
833        queue.set(i, in);
834    }
835
836    /** Reset the insertion mode by searching up the stack for an appropriate insertion mode. */
837    boolean resetInsertionMode() {
838        // https://html.spec.whatwg.org/multipage/parsing.html#the-insertion-mode
839        final HtmlTreeBuilderState origState = this.state;
840
841        if (stack.size() == 0) { // nothing left of stack, just get to body
842            transition(HtmlTreeBuilderState.InBody);
843        }
844
845        LOOP: for (int pos = stack.size() - 1; pos >= 0; pos--) {
846            boolean last = pos == 0;
847            Element node = last && fragmentParsing ? contextElement : stack.get(pos);
848            String name = node != null && isHtmlEl(node) ? node.normalName() : "";
849
850            switch (name) {
851                case "td":
852                case "th":
853                    if (!last) {
854                        transition(HtmlTreeBuilderState.InCell);
855                        break LOOP;
856                    }
857                    break;
858                case "tr":
859                    transition(HtmlTreeBuilderState.InRow);
860                    break LOOP;
861                case "tbody":
862                case "thead":
863                case "tfoot":
864                    transition(HtmlTreeBuilderState.InTableBody);
865                    break LOOP;
866                case "caption":
867                    transition(HtmlTreeBuilderState.InCaption);
868                    break LOOP;
869                case "colgroup":
870                    transition(HtmlTreeBuilderState.InColumnGroup);
871                    break LOOP;
872                case "table":
873                    transition(HtmlTreeBuilderState.InTable);
874                    break LOOP;
875                case "template":
876                    HtmlTreeBuilderState tmplState = currentTemplateMode();
877                    Validate.notNull(tmplState, "Bug: no template insertion mode on stack!");
878                    transition(tmplState);
879                    break LOOP;
880                case "head":
881                    if (!last) {
882                        transition(HtmlTreeBuilderState.InHead);
883                        break LOOP;
884                    }
885                    break;
886                case "body":
887                    transition(HtmlTreeBuilderState.InBody);
888                    break LOOP;
889                case "frameset":
890                    transition(HtmlTreeBuilderState.InFrameset);
891                    break LOOP;
892                case "html":
893                    transition(headElement == null ? HtmlTreeBuilderState.BeforeHead : HtmlTreeBuilderState.AfterHead);
894                    break LOOP;
895            }
896            if (last) {
897                transition(HtmlTreeBuilderState.InBody);
898                break;
899            }
900        }
901        return state != origState;
902    }
903
904    /** Places the body back onto the stack and moves to InBody, for cases in AfterBody / AfterAfterBody when more content comes */
905    void resetBody() {
906        if (!onStack("body")) {
907            stack.add(doc.body()); // not onNodeInserted, as already seen
908        }
909        transition(HtmlTreeBuilderState.InBody);
910    }
911
912    /**
913     Test if the target element is in the requested scope.
914     */
915    private boolean inSpecificScope(String targetName, int boundaryOptions) {
916        // https://html.spec.whatwg.org/multipage/parsing.html#has-an-element-in-the-specific-scope
917        for (int pos = stack.size() - 1; pos >= 0; pos--) {
918            Element el = stack.get(pos);
919            Tag tag = el.tag();
920            if (isHtmlEl(el) && el.normalName().equals(targetName))
921                return true;
922            if (tag.hasParserOption(boundaryOptions))
923                return false;
924        }
925        return false;
926    }
927
928    /**
929     Test if any heading element is in scope.
930     */
931    boolean hasHeadingInScope() {
932        for (int pos = stack.size() - 1; pos >= 0; pos--) {
933            Element el = stack.get(pos);
934            Tag tag = el.tag();
935            if (isHtmlEl(el, Headings))
936                return true;
937            if (tag.hasParserOption(HtmlTagOptions.Scope))
938                return false;
939        }
940        return false;
941    }
942
943    boolean inScope(String targetName) {
944        return inSpecificScope(targetName, HtmlTagOptions.Scope);
945    }
946
947    /** Tests whether this element is on the stack before a scope boundary. */
948    boolean inScope(Element target) {
949        for (int pos = stack.size() - 1; pos >= 0; pos--) {
950            Element el = stack.get(pos);
951            if (el == target) return true;
952            if (el.tag().hasParserOption(HtmlTagOptions.Scope)) return false;
953        }
954        return false;
955    }
956
957    boolean inListItemScope(String targetName) {
958        return inSpecificScope(targetName, HtmlTagOptions.Scope | HtmlTagOptions.ListScope);
959    }
960
961    boolean inButtonScope(String targetName) {
962        return inSpecificScope(targetName, HtmlTagOptions.Scope | HtmlTagOptions.ButtonScope);
963    }
964
965    boolean inTableScope(String targetName) {
966        return inSpecificScope(targetName, HtmlTagOptions.TableScope);
967    }
968
969    /** Tests if there is some element on the stack that is not in the provided set. */
970    boolean onStackNot(String[] allowedTags) {
971        for (int pos = stack.size() - 1; pos >= 0; pos--) {
972            final String elName = stack.get(pos).normalName();
973            if (!inSorted(elName, allowedTags))
974                return true;
975        }
976        return false;
977    }
978
979    void setHeadElement(Element headElement) {
980        this.headElement = headElement;
981    }
982
983    Element getHeadElement() {
984        return headElement;
985    }
986
987    /** Processes a token using the in body insertion mode with foster parenting enabled. */
988    void processWithFosterInserts(Token token) {
989        boolean previous = fosterInserts;
990        fosterInserts = true;
991        process(token, HtmlTreeBuilderState.InBody);
992        fosterInserts = previous;
993    }
994
995    @Nullable FormElement getFormElement() {
996        return formElement;
997    }
998
999    void setFormElement(@Nullable FormElement formElement) {
1000        this.formElement = formElement;
1001    }
1002
1003    private static final class NoscriptState {
1004        final Element boundary;
1005        final @Nullable FormElement savedFormElement;
1006
1007        /**
1008         Captures parser state isolated by the active noscript island.
1009        */
1010        NoscriptState(Element boundary, @Nullable FormElement formElement) {
1011            this.boundary = boundary;
1012            this.savedFormElement = formElement;
1013        }
1014    }
1015
1016    /** Starts a noscript island, preserving parser-global state for restoration on close. */
1017    void startNoscript(Token.StartTag startTag) {
1018        Element boundary = insertElementFor(startTag);
1019        enterNoscript(boundary);
1020    }
1021
1022    /** Enters a noscript island around the provided boundary element. */
1023    private void enterNoscript(Element boundary) {
1024        noscriptState = new NoscriptState(boundary, formElement);
1025        // Fallback form elements should not leak through the parser form pointer.
1026        setFormElement(null);
1027    }
1028
1029    /** Tests if the named element is above the current noscript boundary. */
1030    private boolean inNoscriptScope(String name) {
1031        NoscriptState state = noscriptState;
1032        if (state == null)
1033            return false;
1034        for (int pos = stack.size() - 1; pos >= 0; pos--) {
1035            Element el = stack.get(pos);
1036            if (el == state.boundary)
1037                return false;
1038            if (el.nameIs(name))
1039                return true;
1040        }
1041        return false;
1042    }
1043
1044    /** Closes the active noscript subtree and restores isolated parser state. */
1045    private void endNoscript() {
1046        NoscriptState state = Validate.expectNotNull(noscriptState, "Bug: noscript fallback closed with no island state");
1047        int boundary = noscriptBoundaryIndex(state);
1048        if (boundary == -1) {
1049            error(this.state);
1050            restoreNoscriptState();
1051            return;
1052        }
1053        if (stack.get(stack.size() - 1) != state.boundary)
1054            error(this.state);
1055        while (stack.size() > boundary)
1056            pop();
1057        restoreNoscriptState();
1058    }
1059
1060    /** Finds the active noscript boundary on the stack by identity. */
1061    private int noscriptBoundaryIndex(NoscriptState state) {
1062        for (int pos = stack.size() - 1; pos >= 0; pos--) {
1063            if (stack.get(pos) == state.boundary)
1064                return pos;
1065        }
1066        return -1;
1067    }
1068
1069    /** Restores parser-global state from the active noscript island. */
1070    private void restoreNoscriptState() {
1071        NoscriptState state = Validate.expectNotNull(noscriptState, "Bug: no noscript island state to restore");
1072        noscriptState = null;
1073        setFormElement(state.savedFormElement);
1074    }
1075
1076    void resetPendingTableCharacters() {
1077        pendingTableCharacters.clear();
1078    }
1079
1080    List<Token.Character> getPendingTableCharacters() {
1081        return pendingTableCharacters;
1082    }
1083
1084    void addPendingTableCharacters(Token.Character c) {
1085        // make a copy of the token to maintain its state (as Tokens are otherwise reset)
1086        Token.Character copy = new Token.Character(c);
1087        pendingTableCharacters.add(copy);
1088    }
1089
1090    /**
1091     13.2.6.3 Closing elements that have implied end tags
1092     When the steps below require the UA to generate implied end tags, then, while the current node is a dd element, a dt element, an li element, an optgroup element, an option element, a p element, an rb element, an rp element, an rt element, or an rtc element, the UA must pop the current node off the stack of open elements.
1093
1094     If a step requires the UA to generate implied end tags but lists an element to exclude from the process, then the UA must perform the above steps as if that element was not in the above list.
1095
1096     When the steps below require the UA to generate all implied end tags thoroughly, then, while the current node is a caption element, a colgroup element, a dd element, a dt element, an li element, an optgroup element, an option element, a p element, an rb element, an rp element, an rt element, an rtc element, a tbody element, a td element, a tfoot element, a th element, a thead element, or a tr element, the UA must pop the current node off the stack of open elements.
1097
1098     @param excludeTag If a step requires the UA to generate implied end tags but lists an element to exclude from the
1099     process, then the UA must perform the above steps as if that element was not in the above list.
1100     */
1101    void generateImpliedEndTags(String excludeTag) {
1102        while (hasCurrentElement() && currentElement().tag().hasParserOption(HtmlTagOptions.ImpliedEnd)) {
1103            if (excludeTag != null && currentElementIs(excludeTag))
1104                break;
1105            pop();
1106        }
1107    }
1108
1109    void generateImpliedEndTags() {
1110        generateImpliedEndTags(false);
1111    }
1112
1113    /**
1114     Pops HTML elements off the stack according to the implied end tag rules
1115     @param thorough if we are thorough (includes table elements etc) or not
1116     */
1117    void generateImpliedEndTags(boolean thorough) {
1118        final int option = thorough ? HtmlTagOptions.ThoroughImpliedEnd : HtmlTagOptions.ImpliedEnd;
1119        while (true) {
1120            if (!hasCurrentElement()) break;
1121            Tag tag = currentElement().tag();
1122            if (!tag.hasParserOption(option))
1123                break;
1124            pop();
1125        }
1126    }
1127
1128    void closeElement(String name) {
1129        generateImpliedEndTags(name);
1130        if (!currentElementIs(name)) error(state());
1131        popStackToClose(name);
1132    }
1133
1134    static boolean isSpecial(Element el) {
1135        return el.tag().hasParserOption(HtmlTagOptions.Special);
1136    }
1137
1138    /** Returns the last entry, or null for a scope marker or an empty list. */
1139    @Nullable Element lastFormattingElement() {
1140        return formattingElements.size() > 0 ? formattingElements.get(formattingElements.size()-1) : null;
1141    }
1142
1143    /** Removes the last entry, returning null for a scope marker or an empty list. */
1144    @Nullable Element removeLastFormattingElement() {
1145        int size = formattingElements.size();
1146        if (size > 0)
1147            return formattingElements.remove(size-1);
1148        else
1149            return null;
1150    }
1151
1152    // active formatting elements
1153    void pushActiveFormattingElements(Element in) {
1154        checkActiveFormattingElements(in);
1155        addFormattingElement(formattingElements.size(), in);
1156    }
1157
1158    /** Inserts a formatting element or null scope marker, dropping the oldest entry if over the limit. */
1159    private void addFormattingElement(int pos, @Nullable Element element) {
1160        formattingElements.add(pos, element);
1161        if (formattingElements.size() > MaxFormattingElements)
1162            formattingElements.remove(0);
1163    }
1164
1165    /** Replaces a formatting entry at its original position or after a moved bookmark. */
1166    void replaceFormattingElement(Element out, Element in, Element bookmark) {
1167        int pos = formattingElements.indexOf(bookmark);
1168        if (pos == -1) {
1169            error("Unable to restore formatting order for <%s>", out.tagName());
1170            removeFromActiveFormattingElements(out);
1171            addFormattingElement(formattingElements.size(), in);
1172        } else if (bookmark == out) {
1173            formattingElements.set(pos, in);
1174        } else {
1175            removeFromActiveFormattingElements(out);
1176            addFormattingElement(formattingElements.indexOf(bookmark) + 1, in);
1177        }
1178    }
1179
1180    void checkActiveFormattingElements(Element in){
1181        int numSeen = 0;
1182        final int size = formattingElements.size() -1;
1183        int ceil = size - MaxUsedFormattingElements; if (ceil <0) ceil = 0;
1184
1185        for (int pos = size; pos >= ceil; pos--) {
1186            Element el = formattingElements.get(pos);
1187            if (el == null) // marker
1188                break;
1189
1190            if (isSameFormattingElement(in, el))
1191                numSeen++;
1192
1193            if (numSeen == 3) {
1194                formattingElements.remove(pos);
1195                break;
1196            }
1197        }
1198    }
1199
1200    private static boolean isSameFormattingElement(Element a, Element b) {
1201        // same if: same namespace, tag, and attributes. Element.equals only checks tag, might in future check children
1202        return a.normalName().equals(b.normalName()) &&
1203                // a.namespace().equals(b.namespace()) &&
1204                a.attributes().equals(b.attributes());
1205        // todo: namespaces
1206    }
1207
1208    void reconstructFormattingElements() {
1209        Element last = lastFormattingElement();
1210        if (last == null || onStack(last))
1211            return;
1212
1213        Element entry = last;
1214        int size = formattingElements.size();
1215        int ceil = size - MaxUsedFormattingElements; if (ceil <0) ceil = 0;
1216        int pos = size - 1;
1217        boolean skip = false;
1218        while (true) {
1219            if (pos == ceil) { // step 4. if none before, skip to 8
1220                skip = true;
1221                break;
1222            }
1223            entry = formattingElements.get(--pos); // step 5. one earlier than entry
1224            if (entry == null || onStack(entry)) // step 6 - neither marker nor on stack
1225                break; // jump to 8, else continue back to 4
1226        }
1227        while(true) {
1228            if (!skip) // step 7: on later than entry
1229                entry = formattingElements.get(++pos);
1230            entry = Validate.expectNotNull(entry); // should not occur, as we break at last element
1231
1232            // 8. create new element from element, 9 insert into current node, onto stack
1233            skip = false; // can only skip increment from 4.
1234            Element newEl = recreateElement(entry);
1235            doInsertElement(newEl);
1236
1237            // 10. replace entry with new entry
1238            formattingElements.set(pos, newEl);
1239
1240            // 11
1241            if (pos == size-1) // if not last entry in list, jump to 7
1242                break;
1243        }
1244    }
1245
1246    /** Copies an element's originating source ranges, ready to track a new close. */
1247    Element recreateElement(Element source) {
1248        Element element = new Element(source.tag(), null, source.attributes().clone());
1249        NodeInternals.clearEndSourceRange(element);
1250        return element;
1251    }
1252
1253    static final int MaxFormattingElements = 512; // includes markers, which also consume recovery history
1254    static final int MaxUsedFormattingElements = 12; // limit how many elements get recreated
1255
1256    void clearFormattingElementsToLastMarker() {
1257        while (!formattingElements.isEmpty()) {
1258            Element el = removeLastFormattingElement();
1259            if (el == null)
1260                break;
1261        }
1262    }
1263
1264    void removeFromActiveFormattingElements(Element el) {
1265        for (int pos = formattingElements.size() -1; pos >= 0; pos--) {
1266            Element next = formattingElements.get(pos);
1267            if (next == el) {
1268                formattingElements.remove(pos);
1269                break;
1270            }
1271        }
1272    }
1273
1274    boolean isInActiveFormattingElements(Element el) {
1275        return formattingElements.contains(el);
1276    }
1277
1278    @Nullable
1279    Element getActiveFormattingElement(String nodeName) {
1280        for (int pos = formattingElements.size() -1; pos >= 0; pos--) {
1281            Element next = formattingElements.get(pos);
1282            if (next == null) // scope marker
1283                break;
1284            else if (next.nameIs(nodeName))
1285                return next;
1286        }
1287        return null;
1288    }
1289
1290    void replaceActiveFormattingElement(Element out, Element in) {
1291        replaceInQueue(formattingElements, out, in);
1292    }
1293
1294    void insertMarkerToFormattingElements() {
1295        addFormattingElement(formattingElements.size(), null);
1296    }
1297
1298    // Template Insertion Mode stack
1299    void pushTemplateMode(HtmlTreeBuilderState state) {
1300        tmplInsertMode.add(state);
1301    }
1302
1303    @Nullable HtmlTreeBuilderState popTemplateMode() {
1304        if (tmplInsertMode.size() > 0) {
1305            return tmplInsertMode.remove(tmplInsertMode.size() -1);
1306        } else {
1307            return null;
1308        }
1309    }
1310
1311    int templateModeSize() {
1312        return tmplInsertMode.size();
1313    }
1314
1315    @Nullable HtmlTreeBuilderState currentTemplateMode() {
1316        return (tmplInsertMode.size() > 0) ? tmplInsertMode.get(tmplInsertMode.size() -1)  : null;
1317    }
1318
1319    @Override
1320    public String toString() {
1321        return "TreeBuilder{" +
1322                "currentToken=" + currentToken +
1323                ", state=" + state +
1324                ", currentElement=" + (hasCurrentElement() ? currentElement() : "none") +
1325                '}';
1326    }
1327
1328}