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