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.ProcessingInstruction; 016import org.jsoup.nodes.TextNode; 017import org.jspecify.annotations.Nullable; 018 019import java.io.Reader; 020import java.util.ArrayList; 021import java.util.Collection; 022import java.util.List; 023 024import static org.jsoup.internal.StringUtil.inSorted; 025import static org.jsoup.parser.HtmlTreeBuilderState.Constants.Headings; 026import static org.jsoup.parser.HtmlTreeBuilderState.Constants.InTableFoster; 027import static org.jsoup.parser.HtmlTreeBuilderState.ForeignContent; 028import static org.jsoup.parser.Parser.*; 029 030/** 031 * HTML Tree Builder; creates a DOM from Tokens. 032 */ 033public class HtmlTreeBuilder extends TreeBuilder { 034 static final String[] TagMathMlTextIntegration = new String[]{"mi", "mn", "mo", "ms", "mtext"}; 035 static final String[] TagSvgHtmlIntegration = new String[]{"desc", "foreignObject", "title"}; 036 static final String[] TagFormListed = { 037 "button", "fieldset", "input", "keygen", "object", "output", "select", "textarea" 038 }; 039 040 private HtmlTreeBuilderState state; // the current state 041 private HtmlTreeBuilderState originalState; // original / marked state 042 043 private boolean baseUriSetFromDoc; 044 private @Nullable Element headElement; // the current head element 045 private @Nullable FormElement formElement; // the current form element 046 private @Nullable Element contextElement; // context copy and fragment output container; not on the stack 047 private @Nullable Element fragmentRoot; // internal stack root; contextElement receives fragment nodes 048 ArrayList<@Nullable Element> formattingElements; // active formatting elements and null scope markers 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 fragmentRoot = 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 fragmentRoot = new Element(tagFor("html", "html", NamespaceHtml, settings), baseUri); 128 doc.appendChild(contextElement); 129 push(fragmentRoot); 130 resetInsertionMode(); 131 132 // setup form element to nearest form on context (up ancestor chain). ensures form controls are associated 133 // with form correctly 134 Element formSearch = context; 135 while (formSearch != null) { 136 if (formSearch instanceof FormElement) { 137 formElement = (FormElement) formSearch; 138 break; 139 } 140 formSearch = formSearch.parent(); 141 } 142 143 if (htmlContext && contextName.equals("noscript")) enterNoscript(fragmentRoot); 144 } 145 } 146 147 @Override List<Node> completeParseFragment() { 148 return (contextElement != null ? contextElement : doc).childNodes(); 149 } 150 151 /** 152 Reads a token, separating leading whitespace in insertion modes that handle it separately. 153 */ 154 @Override 155 Token readToken() { 156 // the spec processes Data characters individually; return leading whitespace first 157 // so that the current insertion mode handles it, before following text takes another route 158 if (noscriptState == null && useCurrentOrForeignInsert(tokeniser.charPending)) { 159 switch (state) { 160 case Initial: 161 case BeforeHtml: 162 case BeforeHead: 163 case InHead: 164 case AfterHead: 165 case InColumnGroup: 166 case AfterBody: 167 case AfterAfterBody: 168 case AfterAfterFrameset: 169 return tokeniser.readWhitespace(); 170 } 171 } 172 return tokeniser.read(); 173 } 174 175 @Override 176 protected boolean process(Token token) { 177 if (noscriptState != null && state != HtmlTreeBuilderState.Text) 178 return processNoscriptToken(token); 179 HtmlTreeBuilderState dispatch = useCurrentOrForeignInsert(token) ? this.state : ForeignContent; 180 return dispatch.process(token, this); 181 } 182 183 /** 184 Handles tokens in a noscript island as plain contained markup. This diverges from the spec intentionally so that 185 content is available in the DOM and round-trip serializable, but errant content won't change the parser's context 186 (e.g. an `a` won't kick out of InHead). 187 */ 188 private boolean processNoscriptToken(Token token) { 189 switch (token.type) { 190 case StartTag: 191 return insertNoscriptStartTag(token.asStartTag()); 192 case EndTag: 193 return closeNoscriptEndTag(token.asEndTag()); 194 case Comment: 195 insertCommentNode(token.asComment()); 196 return true; 197 case Character: 198 Token.Character character = token.asCharacter(); 199 insertCharacterNode(character); 200 if (!StringUtil.isBlank(character.getData())) 201 framesetOk(false); 202 return true; 203 case Doctype: 204 error(state); 205 return false; 206 case EOF: 207 error(state); 208 endNoscript(); 209 return process(token); 210 default: 211 Validate.wtf("Unexpected state: " + token.type); // XmlDecl only in XmlTreeBuilder 212 return false; 213 } 214 } 215 216 /** 217 Inserts a start tag inside a noscript island as plain markup. 218 */ 219 private boolean insertNoscriptStartTag(Token.StartTag start) { 220 TokeniserState textState = tagFor(start).textState(); 221 Element el = insertElementFor(start); 222 if (textState != null) { // plaintext is intentionally not TagSet-driven and remains plain fallback markup. 223 if (start.isSelfClosing()) { 224 if (currentElement() == el) 225 pop(); 226 } else { 227 tokeniser.transition(textState); 228 markInsertionMode(); 229 transition(HtmlTreeBuilderState.Text); 230 } 231 } 232 233 framesetOk(false); 234 return true; 235 } 236 237 /** 238 Closes an island element if it matches above the current noscript boundary. 239 */ 240 private boolean closeNoscriptEndTag(Token.EndTag end) { 241 String name = end.normalName(); 242 NoscriptState island = Validate.expectNotNull(noscriptState, "Bug: noscript end tag processed with no island state"); 243 if (name.equals("noscript") && island.boundary != fragmentRoot) { 244 endNoscript(); 245 return true; 246 } 247 if (!inNoscriptScope(name)) { 248 error(state); 249 return false; 250 } 251 if (!currentElementIs(name)) 252 error(state); 253 popStackToClose(name); 254 return true; 255 } 256 257 boolean useCurrentOrForeignInsert(Token token) { 258 // https://html.spec.whatwg.org/multipage/parsing.html#tree-construction 259 // If the stack of open elements is empty 260 if (stack.isEmpty()) 261 return true; 262 final Element el = adjustedCurrentElement(); 263 final String ns = el.tag().namespace(); 264 265 // If the adjusted current node is an element in the HTML namespace 266 if (NamespaceHtml.equals(ns)) 267 return true; 268 269 // 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" 270 // If the adjusted current node is a MathML text integration point and the token is a character token 271 if (isMathmlTextIntegration(el)) { 272 if (token.isStartTag() 273 && !"mglyph".equals(token.asStartTag().normalName) 274 && !"malignmark".equals(token.asStartTag().normalName)) 275 return true; 276 if (token.isCharacter()) 277 return true; 278 } 279 // If the adjusted current node is a MathML annotation-xml element and the token is a start tag whose tag name is "svg" 280 if (Parser.NamespaceMathml.equals(ns) 281 && el.nameIs("annotation-xml") 282 && token.isStartTag() 283 && "svg".equals(token.asStartTag().normalName)) 284 return true; 285 286 // If the adjusted current node is an HTML integration point and the token is a start tag 287 // If the adjusted current node is an HTML integration point and the token is a character token 288 if (isHtmlIntegration(el) 289 && (token.isStartTag() || token.isCharacter())) 290 return true; 291 292 // If the token is an end-of-file token 293 return token.isEOF(); 294 } 295 296 static boolean isMathmlTextIntegration(Element el) { 297 /* 298 A node is a MathML text integration point if it is one of the following elements: 299 A MathML mi element 300 A MathML mo element 301 A MathML mn element 302 A MathML ms element 303 A MathML mtext element 304 */ 305 return (Parser.NamespaceMathml.equals(el.tag().namespace()) 306 && StringUtil.inSorted(el.normalName(), TagMathMlTextIntegration)); 307 } 308 309 /** Tests if an element is in the HTML namespace. */ 310 static boolean isHtmlEl(Element el) { 311 return NamespaceHtml.equals(el.tag().namespace()); 312 } 313 314 /** Tests if an HTML element has the supplied normal name. */ 315 static boolean isHtmlEl(Element el, String normalName) { 316 return isHtmlEl(el) && el.normalName().equals(normalName); 317 } 318 319 /** Tests if an HTML element has one of the supplied sorted normal names. */ 320 static boolean isHtmlEl(Element el, String[] normalNames) { 321 return isHtmlEl(el) && inSorted(el.normalName(), normalNames); 322 } 323 324 static boolean isHtmlIntegration(Element el) { 325 /* 326 A node is an HTML integration point if it is one of the following elements: 327 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" 328 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" 329 An SVG foreignObject element 330 An SVG desc element 331 An SVG title element 332 */ 333 if (Parser.NamespaceMathml.equals(el.tag().namespace()) 334 && el.nameIs("annotation-xml")) { 335 String encoding = Normalizer.asciiLowerCase(el.attr("encoding")); 336 if (encoding.equals("text/html") || encoding.equals("application/xhtml+xml")) 337 return true; 338 } 339 // note using .tagName for case-sensitive hit here of foreignObject 340 return Parser.NamespaceSvg.equals(el.tag().namespace()) && StringUtil.in(el.tagName(), TagSvgHtmlIntegration); 341 } 342 343 boolean process(Token token, HtmlTreeBuilderState state) { 344 return state.process(token, this); 345 } 346 347 void transition(HtmlTreeBuilderState state) { 348 this.state = state; 349 } 350 351 HtmlTreeBuilderState state() { 352 return state; 353 } 354 355 void markInsertionMode() { 356 originalState = state; 357 } 358 359 HtmlTreeBuilderState originalState() { 360 return originalState; 361 } 362 363 void framesetOk(boolean framesetOk) { 364 this.framesetOk = framesetOk; 365 } 366 367 boolean framesetOk() { 368 return framesetOk; 369 } 370 371 Document getDocument() { 372 return doc; 373 } 374 375 String getBaseUri() { 376 return baseUri; 377 } 378 379 void maybeSetBaseUri(Element base) { 380 if (baseUriSetFromDoc) // only listen to the first <base href> in parse 381 return; 382 383 String href = base.absUrl("href"); 384 if (href.length() != 0) { // ignore <base target> etc 385 baseUri = href; 386 baseUriSetFromDoc = true; 387 doc.setBaseUri(href); // set on the doc so doc.createElement(Tag) will get updated base, and to update all descendants 388 } 389 } 390 391 boolean isFragmentParsing() { 392 return fragmentParsing; 393 } 394 395 /** Tests whether this fragment was created with the named HTML context element. */ 396 boolean fragmentContextIs(String normalName) { 397 return fragmentParsing && contextElement != null && isHtmlEl(contextElement, normalName); 398 } 399 400 /** Tests whether the parser is inside a template or using a template fragment context. */ 401 boolean isParsingTemplateContents() { 402 return onStack("template") || fragmentContextIs("template"); 403 } 404 405 void error(HtmlTreeBuilderState state) { 406 if (parser.getErrors().canAddError()) 407 parser.getErrors().add(new ParseError(reader, "Unexpected %s token [%s] when in state [%s]", 408 currentToken.tokenType(), currentToken, state)); 409 } 410 411 /** Creates an element after normalizing its attributes for the selected namespace. */ 412 Element createElementFor(Token.StartTag startTag, String namespace) { 413 // normalize before deduping so differently cased names resolve to one attribute 414 Attributes attributes = startTag.attributes; 415 boolean isHtml = NamespaceHtml.equals(namespace); 416 if (attributes != null && !attributes.isEmpty()) { 417 if (isHtml) 418 settings.normalizeAttributes(attributes); 419 else if (!settings.preserveAttributeCase()) 420 ForeignNames.normalizeAttributes(attributes, namespace); 421 int dupes = attributes.deduplicate(settings); 422 if (dupes > 0) 423 error("Dropped duplicate attribute(s) in tag [%s]", startTag.normalName); 424 startTag.finaliseAttributeRanges(settings); 425 } 426 427 Tag tag = tagFor(startTag.name(), startTag.normalName, namespace, isHtml ? settings : ParseSettings.preserveCase); 428 if (isHtml && tag.normalName().equals("form")) 429 return new FormElement(tag, null, attributes); 430 return new Element(tag, null, attributes); 431 } 432 433 /** Inserts an HTML element for the given tag */ 434 Element insertElementFor(final Token.StartTag startTag) { 435 Element el = createElementFor(startTag, NamespaceHtml); 436 doInsertElement(el); 437 438 // handle self-closing tags. when the spec expects an empty (void) tag, will directly hit insertEmpty, so won't generate this fake end tag. 439 if (startTag.isSelfClosing()) { 440 Tag tag = el.tag(); 441 tag.setSeenSelfClose(); // can infer output if in xml syntax 442 if (tag.isEmpty()) { 443 // treated as empty below; nothing further 444 } else if (tag.isKnownTag() && tag.isSelfClosing()) { 445 // ok, allow it. effectively a pop, but fiddles with the state. handles empty style, title etc which would otherwise leave us in data state 446 tokeniser.transition(TokeniserState.Data); // handles <script />, otherwise needs breakout steps from script data 447 tokeniser.emit(emptyEnd.reset().name(el.tagName())); // ensure we get out of whatever state we are in. emitted for yielded processing 448 } else { 449 // error it, and leave the inserted element on 450 tokeniser.error("Tag [%s] cannot be self-closing; not a void tag", tag.normalName()); 451 } 452 } 453 454 if (el.tag().isEmpty()) { 455 pop(); // custom void tags behave like built-in voids (no children, not left on the stack); known empty go via insertEmpty 456 } 457 458 return el; 459 } 460 461 /** Inserts a foreign element with its configured SVG or MathML name adjustments. */ 462 Element insertForeignElementFor(final Token.StartTag startTag, String namespace) { 463 if (!settings.preserveTagCase()) 464 startTag.name(ForeignNames.tagName(startTag.name(), namespace)); 465 Element el = createElementFor(startTag, namespace); 466 doInsertElement(el); 467 468 if (startTag.isSelfClosing()) { // foreign els are OK to self-close 469 el.tag().setSeenSelfClose(); // remember this is self-closing for output 470 pop(); 471 } 472 473 return el; 474 } 475 476 Element insertEmptyElementFor(Token.StartTag startTag) { 477 Element el = createElementFor(startTag, NamespaceHtml); 478 doInsertElement(el); 479 pop(); 480 return el; 481 } 482 483 /** Inserts a form and leaves the form pointer unset while parsing template contents. */ 484 FormElement insertFormElement(Token.StartTag startTag, boolean onStack) { 485 FormElement el = (FormElement) createElementFor(startTag, NamespaceHtml); 486 if (!isParsingTemplateContents()) setFormElement(el); 487 488 doInsertElement(el); 489 if (!onStack) pop(); 490 return el; 491 } 492 493 /** Inserts the Element onto the stack. All element inserts must run through this method. Performs any general 494 tests on the Element before insertion. 495 * @param el the Element to insert and make the current element 496 */ 497 private void doInsertElement(Element el) { 498 enforceStackDepthLimit(); 499 500 if (formElement != null && isHtmlEl(el, TagFormListed)) 501 formElement.addElement(el); // connect form controls to their form element 502 503 // in HTML, the xmlns attribute if set must match what the parser set the tag's namespace to 504 if (parser.getErrors().canAddError() && el.hasAttr("xmlns") && !el.attr("xmlns").equals(el.tag().namespace())) 505 error("Invalid xmlns attribute [%s] on tag [%s]", el.attr("xmlns"), el.tagName()); 506 507 insertNode(el, currentElOrDoc()); 508 push(el); 509 } 510 511 /** Inserts a node into the target, applying foster parenting when required. */ 512 void insertNode(Node node, Element target) { 513 if (fosterInserts && isHtmlEl(target, InTableFoster)) 514 insertInFosterParent(node); 515 else 516 insertionTarget(target).appendChild(node); 517 } 518 519 /** Inserts a node at the foster parenting location. */ 520 void insertInFosterParent(Node node) { 521 for (int pos = stack.size() - 1; pos >= 0; pos--) { 522 Element el = stack.get(pos); 523 if (isHtmlEl(el, "template")) { 524 el.appendChild(node); // template contents are stored on the element 525 return; 526 } 527 if (isHtmlEl(el, "table")) { 528 Element parent = el.parent(); 529 if (parent != null) 530 el.before(node); 531 else 532 insertionTarget(stack.get(pos - 1)).appendChild(node); 533 return; 534 } 535 } 536 // https://html.spec.whatwg.org/multipage/parsing.html#appropriate-place-for-inserting-a-node 537 // fragment case: insert into the first element on the stack (the html element) 538 insertionTarget(stack.get(0)).appendChild(node); 539 } 540 541 /** Moves a node for adoption agency recovery to its adjusted insertion location. */ 542 void insertAdopted(Node node, Element target) { 543 if (fosterInserts && isHtmlEl(target, InTableFoster)) { 544 for (int pos = stack.size() - 1; pos >= 0; pos--) { 545 Element el = stack.get(pos); 546 if (isHtmlEl(el, "template")) { 547 insertAdopted(node, el, null); // template contents are stored on the element 548 return; 549 } 550 if (isHtmlEl(el, "table")) { 551 Element parent = el.parent(); 552 if (parent != null) 553 insertAdopted(node, parent, el); 554 else 555 insertAdopted(node, insertionTarget(stack.get(pos - 1)), null); 556 return; 557 } 558 } 559 target = stack.get(0); 560 } 561 insertAdopted(node, insertionTarget(target), null); 562 } 563 564 /** 565 Moves a node to a resolved parent and position, subject to the DOM insertion validity rules. 566 @param node the node to remove and reinsert 567 @param parent the element to insert the node into 568 @param before a child of {@code parent} to insert the node before, or {@code null} to append the node 569 */ 570 static void insertAdopted(Node node, Element parent, @Nullable Node before) { 571 // https://html.spec.whatwg.org/multipage/parsing.html#adoption-agency-algorithm 572 // 4.15: If lastNode's parent is non-null, then remove lastNode. 573 if (node.parent() != null) node.remove(); 574 // 4.16: Insert lastNode into target before refNode only if pre-insert validity holds. 575 if (node.parent() != null || (before != null && before.parent() != parent)) return; 576 if (parent instanceof Document && parent.childrenSize() != 0) return; 577 for (Node ancestor = parent; ancestor != null; ancestor = ancestor.parent()) { 578 if (ancestor == node) return; 579 } 580 if (before == null) 581 parent.appendChild(node); 582 else 583 before.before(node); 584 } 585 586 /** Inserts a comment into the current element, or the document when there is none. */ 587 void insertCommentNode(Token.Comment token) { 588 insertCommentNode(token, currentElOrDoc()); 589 } 590 591 /** Inserts a comment into the supplied target. */ 592 void insertCommentNode(Token.Comment token, Element target) { 593 Node node; 594 if (token.isPI()) { 595 Token.PI piToken = token.asPI(); 596 ProcessingInstruction instruction = new ProcessingInstruction(piToken.target(), token.getData()); 597 NodeInternals.sourceDataStart(instruction, piToken.dataStartPos); 598 node = instruction; 599 } else { 600 node = new Comment(token.getData()); 601 } 602 insertionTarget(target).appendChild(node); 603 onNodeInserted(node); 604 } 605 606 /** Inserts the provided character token into the current element, or the document when there is none. */ 607 void insertCharacterNode(Token.Character characterToken) { 608 insertCharacterNode(characterToken, false); 609 } 610 611 /** 612 Inserts the provided character token into the current element, or the document when there is none. 613 The tokenizer will have already raised precise character errors. 614 615 @param characterToken the character token to insert 616 @param replace if true, replaces any null chars in the data with the replacement char (U+FFFD). If false, removes 617 null chars. 618 */ 619 void insertCharacterNode(Token.Character characterToken, boolean replace) { 620 characterToken.normalizeNulls(replace); 621 if (characterToken.getData().isEmpty()) return; 622 Element target = currentElOrDoc(); 623 Node node = createCharacterNode(characterToken, insertionTarget(target)); 624 insertNode(node, target); 625 onNodeInserted(node); 626 } 627 628 /** Inserts the provided character token into the provided element. */ 629 void insertCharacterToElement(Token.Character characterToken, Element el) { 630 Element target = insertionTarget(el); 631 Node node = createCharacterNode(characterToken, target); 632 target.appendChild(node); // insert at the requested element, without foster parenting 633 onNodeInserted(node); 634 } 635 636 /** Creates the text or data node represented by a character token. */ 637 private Node createCharacterNode(Token.Character characterToken, Element target) { 638 final String data = characterToken.getData(); 639 640 if (characterToken.isCData()) 641 return new CDataNode(data); 642 else if (target.tag().is(Tag.Data)) 643 return new DataNode(data); 644 else 645 return new TextNode(data); 646 } 647 648 ArrayList<Element> getStack() { 649 return stack; 650 } 651 652 /** Gets the insertion target for fragment content. */ 653 private Element insertionTarget(Element target) { 654 // the html root is only used by the tree builder; content belongs in the context copy 655 return target == fragmentRoot && contextElement != null ? contextElement : target; 656 } 657 658 /** Notifies listeners that a node was inserted. */ 659 @Override void onNodeInserted(Node node) { 660 // listeners observe the fragment's context copy, not its parser-only html root 661 super.onNodeInserted(node == fragmentRoot && contextElement != null ? contextElement : node); 662 } 663 664 /** Notifies listeners that a node was closed. */ 665 @Override void onNodeClosed(Node node) { 666 super.onNodeClosed(node == fragmentRoot && contextElement != null ? contextElement : node); 667 } 668 669 /** Tests whether an element is open. */ 670 @Override boolean isOpen(Element element) { 671 // the context copy remains open for streaming while its parser root is open 672 return super.isOpen(element == contextElement && fragmentRoot != null ? fragmentRoot : element); 673 } 674 675 /** Copies the open elements. */ 676 @Override void copyOpenElementsTo(Collection<? super Element> elements) { 677 // streaming tracks the context copy in place of the parser-only root 678 for (Element element : stack) 679 elements.add(insertionTarget(element)); 680 } 681 682 boolean onStack(Element el) { 683 return stack.contains(el); 684 } 685 686 /** Checks if there is an HTML element with the given name on the stack. */ 687 boolean onStack(String elName) { 688 return getFromStack(elName) != null; 689 } 690 691 /** Gets the adjusted current element for foreign-content dispatch. */ 692 private Element adjustedCurrentElement() { 693 // fragment parsing uses the context at the parser root 694 return fragmentParsing && stack.size() == 1 && contextElement != null ? contextElement : currentElement(); 695 } 696 697 /** Gets the namespace used to process the current token. */ 698 @Override String currentElNs() { 699 return adjustedCurrentElement().tag().namespace(); 700 } 701 702 /** Gets the nearest (lowest) HTML element with the given name from the stack. */ 703 @Nullable 704 Element getFromStack(String elName) { 705 for (int pos = stack.size() - 1; pos >= 0; pos--) { 706 Element next = stack.get(pos); 707 if (isHtmlEl(next, elName)) { 708 return next; 709 } 710 } 711 return null; 712 } 713 714 boolean removeFromStack(Element el) { 715 for (int pos = stack.size() -1; pos >= 0; pos--) { 716 Element next = stack.get(pos); 717 if (next == el) { 718 stack.remove(pos); 719 onNodeClosed(el); 720 return true; 721 } 722 } 723 return false; 724 } 725 726 @Override 727 void onStackPrunedForDepth(Element element) { 728 // handle other effects of popping to keep state correct 729 if (element == headElement) headElement = null; 730 if (element == formElement) setFormElement(null); 731 removeFromActiveFormattingElements(element); 732 if (element.nameIs("template")) { 733 clearFormattingElementsToLastMarker(); 734 if (templateModeSize() > 0) 735 popTemplateMode(); 736 resetInsertionMode(); 737 } else if (noscriptState != null && element == noscriptState.boundary) { 738 restoreNoscriptState(); 739 } 740 } 741 742 /** Pops the stack until the given HTML element is removed. */ 743 @Nullable 744 Element popStackToClose(String elName) { 745 for (int pos = stack.size() -1; pos >= 0; pos--) { 746 Element el = pop(); 747 if (isHtmlEl(el, elName)) { 748 return el; 749 } 750 } 751 return null; 752 } 753 754 /** Pops the stack until an element with the supplied name is removed, irrespective of namespace. */ 755 @Nullable 756 Element popStackToCloseAnyNamespace(String elName) { 757 for (int pos = stack.size() -1; pos >= 0; pos--) { 758 Element el = pop(); 759 if (el.nameIs(elName)) { 760 return el; 761 } 762 } 763 return null; 764 } 765 766 /** Pops the stack until one of the given HTML elements is removed. */ 767 void popStackToClose(String... elNames) { // elnames is sorted, comes from Constants 768 for (int pos = stack.size() -1; pos >= 0; pos--) { 769 Element el = pop(); 770 if (isHtmlEl(el, elNames)) { 771 break; 772 } 773 } 774 } 775 776 void clearStackToTableContext() { 777 clearStackToContext("table", "template"); 778 } 779 780 void clearStackToTableBodyContext() { 781 clearStackToContext("tbody", "tfoot", "thead", "template"); 782 } 783 784 void clearStackToTableRowContext() { 785 clearStackToContext("tr", "template"); 786 } 787 788 /** Removes elements from the stack until one of the supplied HTML elements is removed. */ 789 private void clearStackToContext(String... nodeNames) { 790 for (int pos = stack.size() -1; pos >= 0; pos--) { 791 Element next = stack.get(pos); 792 if (isHtmlEl(next) && 793 (StringUtil.in(next.normalName(), nodeNames) || next.nameIs("html"))) 794 break; 795 else 796 pop(); 797 } 798 } 799 800 /** 801 Gets the Element immediately above the supplied element on the stack. Which due to adoption, may not necessarily be 802 its parent. 803 804 @param el 805 @return the Element immediately above the supplied element, or null if there is no such element. 806 */ 807 @Nullable Element aboveOnStack(Element el) { 808 if (!onStack(el)) return null; 809 for (int pos = stack.size() -1; pos > 0; pos--) { 810 Element next = stack.get(pos); 811 if (next == el) { 812 return stack.get(pos-1); 813 } 814 } 815 return null; 816 } 817 818 /** Inserts an element immediately after an existing stack entry. */ 819 void insertOnStackAfter(Element after, Element in) { 820 int i = stack.lastIndexOf(after); 821 if (i == -1) { 822 error("Unable to place <%s> after <%s> while recovering misnested formatting", in.tagName(), after.tagName()); 823 stack.add(in); 824 } else { 825 stack.add(i + 1, in); 826 } 827 } 828 829 /** Replaces an existing element without changing any scope markers. */ 830 private static void replaceInQueue(ArrayList<@Nullable Element> queue, Element out, Element in) { 831 int i = queue.lastIndexOf(out); 832 Validate.isTrue(i != -1); 833 queue.set(i, in); 834 } 835 836 /** Reset the insertion mode by searching up the stack for an appropriate insertion mode. */ 837 boolean resetInsertionMode() { 838 // https://html.spec.whatwg.org/multipage/parsing.html#the-insertion-mode 839 final HtmlTreeBuilderState origState = this.state; 840 841 if (stack.size() == 0) { // nothing left of stack, just get to body 842 transition(HtmlTreeBuilderState.InBody); 843 } 844 845 LOOP: for (int pos = stack.size() - 1; pos >= 0; pos--) { 846 boolean last = pos == 0; 847 Element node = last && fragmentParsing ? contextElement : stack.get(pos); 848 String name = node != null && isHtmlEl(node) ? node.normalName() : ""; 849 850 switch (name) { 851 case "td": 852 case "th": 853 if (!last) { 854 transition(HtmlTreeBuilderState.InCell); 855 break LOOP; 856 } 857 break; 858 case "tr": 859 transition(HtmlTreeBuilderState.InRow); 860 break LOOP; 861 case "tbody": 862 case "thead": 863 case "tfoot": 864 transition(HtmlTreeBuilderState.InTableBody); 865 break LOOP; 866 case "caption": 867 transition(HtmlTreeBuilderState.InCaption); 868 break LOOP; 869 case "colgroup": 870 transition(HtmlTreeBuilderState.InColumnGroup); 871 break LOOP; 872 case "table": 873 transition(HtmlTreeBuilderState.InTable); 874 break LOOP; 875 case "template": 876 HtmlTreeBuilderState tmplState = currentTemplateMode(); 877 Validate.notNull(tmplState, "Bug: no template insertion mode on stack!"); 878 transition(tmplState); 879 break LOOP; 880 case "head": 881 if (!last) { 882 transition(HtmlTreeBuilderState.InHead); 883 break LOOP; 884 } 885 break; 886 case "body": 887 transition(HtmlTreeBuilderState.InBody); 888 break LOOP; 889 case "frameset": 890 transition(HtmlTreeBuilderState.InFrameset); 891 break LOOP; 892 case "html": 893 transition(headElement == null ? HtmlTreeBuilderState.BeforeHead : HtmlTreeBuilderState.AfterHead); 894 break LOOP; 895 } 896 if (last) { 897 transition(HtmlTreeBuilderState.InBody); 898 break; 899 } 900 } 901 return state != origState; 902 } 903 904 /** Places the body back onto the stack and moves to InBody, for cases in AfterBody / AfterAfterBody when more content comes */ 905 void resetBody() { 906 if (!onStack("body")) { 907 stack.add(doc.body()); // not onNodeInserted, as already seen 908 } 909 transition(HtmlTreeBuilderState.InBody); 910 } 911 912 /** 913 Test if the target element is in the requested scope. 914 */ 915 private boolean inSpecificScope(String targetName, int boundaryOptions) { 916 // https://html.spec.whatwg.org/multipage/parsing.html#has-an-element-in-the-specific-scope 917 for (int pos = stack.size() - 1; pos >= 0; pos--) { 918 Element el = stack.get(pos); 919 Tag tag = el.tag(); 920 if (isHtmlEl(el) && el.normalName().equals(targetName)) 921 return true; 922 if (tag.hasParserOption(boundaryOptions)) 923 return false; 924 } 925 return false; 926 } 927 928 /** 929 Test if any heading element is in scope. 930 */ 931 boolean hasHeadingInScope() { 932 for (int pos = stack.size() - 1; pos >= 0; pos--) { 933 Element el = stack.get(pos); 934 Tag tag = el.tag(); 935 if (isHtmlEl(el, Headings)) 936 return true; 937 if (tag.hasParserOption(HtmlTagOptions.Scope)) 938 return false; 939 } 940 return false; 941 } 942 943 boolean inScope(String targetName) { 944 return inSpecificScope(targetName, HtmlTagOptions.Scope); 945 } 946 947 /** Tests whether this element is on the stack before a scope boundary. */ 948 boolean inScope(Element target) { 949 for (int pos = stack.size() - 1; pos >= 0; pos--) { 950 Element el = stack.get(pos); 951 if (el == target) return true; 952 if (el.tag().hasParserOption(HtmlTagOptions.Scope)) return false; 953 } 954 return false; 955 } 956 957 boolean inListItemScope(String targetName) { 958 return inSpecificScope(targetName, HtmlTagOptions.Scope | HtmlTagOptions.ListScope); 959 } 960 961 boolean inButtonScope(String targetName) { 962 return inSpecificScope(targetName, HtmlTagOptions.Scope | HtmlTagOptions.ButtonScope); 963 } 964 965 boolean inTableScope(String targetName) { 966 return inSpecificScope(targetName, HtmlTagOptions.TableScope); 967 } 968 969 /** Tests if there is some element on the stack that is not in the provided set. */ 970 boolean onStackNot(String[] allowedTags) { 971 for (int pos = stack.size() - 1; pos >= 0; pos--) { 972 final String elName = stack.get(pos).normalName(); 973 if (!inSorted(elName, allowedTags)) 974 return true; 975 } 976 return false; 977 } 978 979 void setHeadElement(Element headElement) { 980 this.headElement = headElement; 981 } 982 983 Element getHeadElement() { 984 return headElement; 985 } 986 987 /** Processes a token using the in body insertion mode with foster parenting enabled. */ 988 void processWithFosterInserts(Token token) { 989 boolean previous = fosterInserts; 990 fosterInserts = true; 991 process(token, HtmlTreeBuilderState.InBody); 992 fosterInserts = previous; 993 } 994 995 @Nullable FormElement getFormElement() { 996 return formElement; 997 } 998 999 void setFormElement(@Nullable FormElement formElement) { 1000 this.formElement = formElement; 1001 } 1002 1003 private static final class NoscriptState { 1004 final Element boundary; 1005 final @Nullable FormElement savedFormElement; 1006 1007 /** 1008 Captures parser state isolated by the active noscript island. 1009 */ 1010 NoscriptState(Element boundary, @Nullable FormElement formElement) { 1011 this.boundary = boundary; 1012 this.savedFormElement = formElement; 1013 } 1014 } 1015 1016 /** Starts a noscript island, preserving parser-global state for restoration on close. */ 1017 void startNoscript(Token.StartTag startTag) { 1018 Element boundary = insertElementFor(startTag); 1019 enterNoscript(boundary); 1020 } 1021 1022 /** Enters a noscript island around the provided boundary element. */ 1023 private void enterNoscript(Element boundary) { 1024 noscriptState = new NoscriptState(boundary, formElement); 1025 // Fallback form elements should not leak through the parser form pointer. 1026 setFormElement(null); 1027 } 1028 1029 /** Tests if the named element is above the current noscript boundary. */ 1030 private boolean inNoscriptScope(String name) { 1031 NoscriptState state = noscriptState; 1032 if (state == null) 1033 return false; 1034 for (int pos = stack.size() - 1; pos >= 0; pos--) { 1035 Element el = stack.get(pos); 1036 if (el == state.boundary) 1037 return false; 1038 if (el.nameIs(name)) 1039 return true; 1040 } 1041 return false; 1042 } 1043 1044 /** Closes the active noscript subtree and restores isolated parser state. */ 1045 private void endNoscript() { 1046 NoscriptState state = Validate.expectNotNull(noscriptState, "Bug: noscript fallback closed with no island state"); 1047 int boundary = noscriptBoundaryIndex(state); 1048 if (boundary == -1) { 1049 error(this.state); 1050 restoreNoscriptState(); 1051 return; 1052 } 1053 if (stack.get(stack.size() - 1) != state.boundary) 1054 error(this.state); 1055 while (stack.size() > boundary) 1056 pop(); 1057 restoreNoscriptState(); 1058 } 1059 1060 /** Finds the active noscript boundary on the stack by identity. */ 1061 private int noscriptBoundaryIndex(NoscriptState state) { 1062 for (int pos = stack.size() - 1; pos >= 0; pos--) { 1063 if (stack.get(pos) == state.boundary) 1064 return pos; 1065 } 1066 return -1; 1067 } 1068 1069 /** Restores parser-global state from the active noscript island. */ 1070 private void restoreNoscriptState() { 1071 NoscriptState state = Validate.expectNotNull(noscriptState, "Bug: no noscript island state to restore"); 1072 noscriptState = null; 1073 setFormElement(state.savedFormElement); 1074 } 1075 1076 void resetPendingTableCharacters() { 1077 pendingTableCharacters.clear(); 1078 } 1079 1080 List<Token.Character> getPendingTableCharacters() { 1081 return pendingTableCharacters; 1082 } 1083 1084 void addPendingTableCharacters(Token.Character c) { 1085 // make a copy of the token to maintain its state (as Tokens are otherwise reset) 1086 Token.Character copy = new Token.Character(c); 1087 pendingTableCharacters.add(copy); 1088 } 1089 1090 /** 1091 13.2.6.3 Closing elements that have implied end tags 1092 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. 1093 1094 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. 1095 1096 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. 1097 1098 @param excludeTag If a step requires the UA to generate implied end tags but lists an element to exclude from the 1099 process, then the UA must perform the above steps as if that element was not in the above list. 1100 */ 1101 void generateImpliedEndTags(String excludeTag) { 1102 while (hasCurrentElement() && currentElement().tag().hasParserOption(HtmlTagOptions.ImpliedEnd)) { 1103 if (excludeTag != null && currentElementIs(excludeTag)) 1104 break; 1105 pop(); 1106 } 1107 } 1108 1109 void generateImpliedEndTags() { 1110 generateImpliedEndTags(false); 1111 } 1112 1113 /** 1114 Pops HTML elements off the stack according to the implied end tag rules 1115 @param thorough if we are thorough (includes table elements etc) or not 1116 */ 1117 void generateImpliedEndTags(boolean thorough) { 1118 final int option = thorough ? HtmlTagOptions.ThoroughImpliedEnd : HtmlTagOptions.ImpliedEnd; 1119 while (true) { 1120 if (!hasCurrentElement()) break; 1121 Tag tag = currentElement().tag(); 1122 if (!tag.hasParserOption(option)) 1123 break; 1124 pop(); 1125 } 1126 } 1127 1128 void closeElement(String name) { 1129 generateImpliedEndTags(name); 1130 if (!currentElementIs(name)) error(state()); 1131 popStackToClose(name); 1132 } 1133 1134 static boolean isSpecial(Element el) { 1135 return el.tag().hasParserOption(HtmlTagOptions.Special); 1136 } 1137 1138 /** Returns the last entry, or null for a scope marker or an empty list. */ 1139 @Nullable Element lastFormattingElement() { 1140 return formattingElements.size() > 0 ? formattingElements.get(formattingElements.size()-1) : null; 1141 } 1142 1143 /** Removes the last entry, returning null for a scope marker or an empty list. */ 1144 @Nullable Element removeLastFormattingElement() { 1145 int size = formattingElements.size(); 1146 if (size > 0) 1147 return formattingElements.remove(size-1); 1148 else 1149 return null; 1150 } 1151 1152 // active formatting elements 1153 void pushActiveFormattingElements(Element in) { 1154 checkActiveFormattingElements(in); 1155 addFormattingElement(formattingElements.size(), in); 1156 } 1157 1158 /** Inserts a formatting element or null scope marker, dropping the oldest entry if over the limit. */ 1159 private void addFormattingElement(int pos, @Nullable Element element) { 1160 formattingElements.add(pos, element); 1161 if (formattingElements.size() > MaxFormattingElements) 1162 formattingElements.remove(0); 1163 } 1164 1165 /** Replaces a formatting entry at its original position or after a moved bookmark. */ 1166 void replaceFormattingElement(Element out, Element in, Element bookmark) { 1167 int pos = formattingElements.indexOf(bookmark); 1168 if (pos == -1) { 1169 error("Unable to restore formatting order for <%s>", out.tagName()); 1170 removeFromActiveFormattingElements(out); 1171 addFormattingElement(formattingElements.size(), in); 1172 } else if (bookmark == out) { 1173 formattingElements.set(pos, in); 1174 } else { 1175 removeFromActiveFormattingElements(out); 1176 addFormattingElement(formattingElements.indexOf(bookmark) + 1, in); 1177 } 1178 } 1179 1180 void checkActiveFormattingElements(Element in){ 1181 int numSeen = 0; 1182 final int size = formattingElements.size() -1; 1183 int ceil = size - MaxUsedFormattingElements; if (ceil <0) ceil = 0; 1184 1185 for (int pos = size; pos >= ceil; pos--) { 1186 Element el = formattingElements.get(pos); 1187 if (el == null) // marker 1188 break; 1189 1190 if (isSameFormattingElement(in, el)) 1191 numSeen++; 1192 1193 if (numSeen == 3) { 1194 formattingElements.remove(pos); 1195 break; 1196 } 1197 } 1198 } 1199 1200 private static boolean isSameFormattingElement(Element a, Element b) { 1201 // same if: same namespace, tag, and attributes. Element.equals only checks tag, might in future check children 1202 return a.normalName().equals(b.normalName()) && 1203 // a.namespace().equals(b.namespace()) && 1204 a.attributes().equals(b.attributes()); 1205 // todo: namespaces 1206 } 1207 1208 void reconstructFormattingElements() { 1209 Element last = lastFormattingElement(); 1210 if (last == null || onStack(last)) 1211 return; 1212 1213 Element entry = last; 1214 int size = formattingElements.size(); 1215 int ceil = size - MaxUsedFormattingElements; if (ceil <0) ceil = 0; 1216 int pos = size - 1; 1217 boolean skip = false; 1218 while (true) { 1219 if (pos == ceil) { // step 4. if none before, skip to 8 1220 skip = true; 1221 break; 1222 } 1223 entry = formattingElements.get(--pos); // step 5. one earlier than entry 1224 if (entry == null || onStack(entry)) // step 6 - neither marker nor on stack 1225 break; // jump to 8, else continue back to 4 1226 } 1227 while(true) { 1228 if (!skip) // step 7: on later than entry 1229 entry = formattingElements.get(++pos); 1230 entry = Validate.expectNotNull(entry); // should not occur, as we break at last element 1231 1232 // 8. create new element from element, 9 insert into current node, onto stack 1233 skip = false; // can only skip increment from 4. 1234 Element newEl = recreateElement(entry); 1235 doInsertElement(newEl); 1236 1237 // 10. replace entry with new entry 1238 formattingElements.set(pos, newEl); 1239 1240 // 11 1241 if (pos == size-1) // if not last entry in list, jump to 7 1242 break; 1243 } 1244 } 1245 1246 /** Copies an element's originating source ranges, ready to track a new close. */ 1247 Element recreateElement(Element source) { 1248 Element element = new Element(source.tag(), null, source.attributes().clone()); 1249 NodeInternals.clearEndSourceRange(element); 1250 return element; 1251 } 1252 1253 static final int MaxFormattingElements = 512; // includes markers, which also consume recovery history 1254 static final int MaxUsedFormattingElements = 12; // limit how many elements get recreated 1255 1256 void clearFormattingElementsToLastMarker() { 1257 while (!formattingElements.isEmpty()) { 1258 Element el = removeLastFormattingElement(); 1259 if (el == null) 1260 break; 1261 } 1262 } 1263 1264 void removeFromActiveFormattingElements(Element el) { 1265 for (int pos = formattingElements.size() -1; pos >= 0; pos--) { 1266 Element next = formattingElements.get(pos); 1267 if (next == el) { 1268 formattingElements.remove(pos); 1269 break; 1270 } 1271 } 1272 } 1273 1274 boolean isInActiveFormattingElements(Element el) { 1275 return formattingElements.contains(el); 1276 } 1277 1278 @Nullable 1279 Element getActiveFormattingElement(String nodeName) { 1280 for (int pos = formattingElements.size() -1; pos >= 0; pos--) { 1281 Element next = formattingElements.get(pos); 1282 if (next == null) // scope marker 1283 break; 1284 else if (next.nameIs(nodeName)) 1285 return next; 1286 } 1287 return null; 1288 } 1289 1290 void replaceActiveFormattingElement(Element out, Element in) { 1291 replaceInQueue(formattingElements, out, in); 1292 } 1293 1294 void insertMarkerToFormattingElements() { 1295 addFormattingElement(formattingElements.size(), null); 1296 } 1297 1298 // Template Insertion Mode stack 1299 void pushTemplateMode(HtmlTreeBuilderState state) { 1300 tmplInsertMode.add(state); 1301 } 1302 1303 @Nullable HtmlTreeBuilderState popTemplateMode() { 1304 if (tmplInsertMode.size() > 0) { 1305 return tmplInsertMode.remove(tmplInsertMode.size() -1); 1306 } else { 1307 return null; 1308 } 1309 } 1310 1311 int templateModeSize() { 1312 return tmplInsertMode.size(); 1313 } 1314 1315 @Nullable HtmlTreeBuilderState currentTemplateMode() { 1316 return (tmplInsertMode.size() > 0) ? tmplInsertMode.get(tmplInsertMode.size() -1) : null; 1317 } 1318 1319 @Override 1320 public String toString() { 1321 return "TreeBuilder{" + 1322 "currentToken=" + currentToken + 1323 ", state=" + state + 1324 ", currentElement=" + (hasCurrentElement() ? currentElement() : "none") + 1325 '}'; 1326 } 1327 1328}