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