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