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