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}