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