diff --git a/gwt-src/nu/validator/htmlparser/gwt/BrowserTreeBuilder.java b/gwt-src/nu/validator/htmlparser/gwt/BrowserTreeBuilder.java index 2eaa6764..26905cce 100644 --- a/gwt-src/nu/validator/htmlparser/gwt/BrowserTreeBuilder.java +++ b/gwt-src/nu/validator/htmlparser/gwt/BrowserTreeBuilder.java @@ -474,4 +474,84 @@ private static native void removeChild(JavaScriptObject parent, fatal(e); } } + + private static native JavaScriptObject getNextSibling( + JavaScriptObject node) /*-{ + return node.nextSibling; + }-*/; + + private static native String getLocalName( + JavaScriptObject node) /*-{ + return node.localName; + }-*/; + + private static native boolean hasAttribute( + JavaScriptObject node, String name) /*-{ + return node.hasAttribute(name); + }-*/; + + @Override + // https://html.spec.whatwg.org/multipage/form-elements.html#concept-select-option-selectedness + // Implements "maybe clone an option into selectedcontent" + protected void optionElementPopped(JavaScriptObject option) + throws SAXException { + try { + // Find the nearest ancestor + JavaScriptObject selectedContent = findDescendantByLocalName( + select, "selectedcontent"); + if (selectedContent == null) { + return; + } + + // Check option selectedness + boolean hasSelectedAttr = hasAttribute(option, "selected"); + if (!hasSelectedAttr && hasChildNodes(selectedContent)) { + // Not the first option and no explicit selected attr + return; + } + + // Clear selectedcontent children and deep-clone option children + while (hasChildNodes(selectedContent)) { + removeChild(selectedContent, getFirstChild(selectedContent)); + } + for (JavaScriptObject child = getFirstChild(option); + child != null; child = getNextSibling(child)) { + appendChild(selectedContent, cloneNodeDeep(child)); + } + } catch (JavaScriptException e) { + fatal(e); + } + } + + private JavaScriptObject findDescendantByLocalName( + JavaScriptObject root, String localName) { + for (JavaScriptObject child = getFirstChild(root); + child != null; child = getNextSibling(child)) { + if (getNodeType(child) == 1) { + if (localName.equals(getLocalName(child))) { + return child; + } + JavaScriptObject found = findDescendantByLocalName( + child, localName); + if (found != null) { + return found; + } + } + } + return null; + } } diff --git a/src/nu/validator/htmlparser/dom/DOMTreeBuilder.java b/src/nu/validator/htmlparser/dom/DOMTreeBuilder.java index 7470a151..84f0e90b 100644 --- a/src/nu/validator/htmlparser/dom/DOMTreeBuilder.java +++ b/src/nu/validator/htmlparser/dom/DOMTreeBuilder.java @@ -354,4 +354,73 @@ protected Element createAndInsertFosterParentedElement(String ns, String name, fatal(e); } } + + @Override + // https://html.spec.whatwg.org/multipage/form-elements.html#concept-select-option-selectedness + // Implements "maybe clone an option into selectedcontent" + protected void optionElementPopped(Element option) throws SAXException { + try { + // Find the nearest ancestor + Element selectedContent = findSelectedContent(select); + if (selectedContent == null) { + return; + } + + // Check option selectedness + boolean hasSelected = option.hasAttribute("selected"); + if (!hasSelected && selectedContent.hasChildNodes()) { + // Not the first option and no explicit selected attr + return; + } + + // Clear selectedcontent children and deep-clone option children + while (selectedContent.hasChildNodes()) { + selectedContent.removeChild(selectedContent.getFirstChild()); + } + for (Node child = option.getFirstChild(); child != null; + child = child.getNextSibling()) { + selectedContent.appendChild(child.cloneNode(true)); + } + } catch (DOMException e) { + fatal(e); + } + } + + private Element findSelectedContent(Element root) { + for (Node child = root.getFirstChild(); child != null; + child = child.getNextSibling()) { + if (child.getNodeType() == Node.ELEMENT_NODE) { + Element elt = (Element) child; + if ("selectedcontent".equals(elt.getLocalName()) + && "http://www.w3.org/1999/xhtml".equals( + elt.getNamespaceURI())) { + return elt; + } + Element found = findSelectedContent(elt); + if (found != null) { + return found; + } + } + } + return null; + } } diff --git a/src/nu/validator/htmlparser/impl/ElementName.java b/src/nu/validator/htmlparser/impl/ElementName.java index 2d09c338..b52ca430 100644 --- a/src/nu/validator/htmlparser/impl/ElementName.java +++ b/src/nu/validator/htmlparser/impl/ElementName.java @@ -1424,7 +1424,11 @@ public void destructor() { public static final ElementName SELECT = new ElementName("select", "select", // CPPONLY: NS_NewHTMLSelectElement, // CPPONLY: NS_NewSVGUnknownElement, -TreeBuilder.SELECT | SPECIAL); +TreeBuilder.SELECT | SPECIAL | SCOPING); +public static final ElementName SELECTEDCONTENT = new ElementName("selectedcontent", "selectedcontent", +// CPPONLY: NS_NewHTMLElement, +// CPPONLY: NS_NewSVGUnknownElement, +TreeBuilder.OTHER); public static final ElementName SLOT = new ElementName("slot", "slot", // CPPONLY: NS_NewHTMLSlotElement, // CPPONLY: NS_NewSVGUnknownElement, @@ -1484,18 +1488,18 @@ public void destructor() { private final static @NoLength ElementName[] ELEMENT_NAMES = { FIGCAPTION, CITE, -FRAMESET, +FEOFFSET, H1, CLIPPATH, METER, -RADIALGRADIENT, +SELECT, B, BGSOUND, SOURCE, DL, RP, -NOFRAMES, -MTEXT, +PROGRESS, +NOSCRIPT, VIEW, DIV, G, @@ -1507,10 +1511,10 @@ public void destructor() { ANIMATETRANSFORM, SECTION, HR, -CANVAS, -BASEFONT, -FEDISTANTLIGHT, -OUTPUT, +DEFS, +DATALIST, +FONT, +PLAINTEXT, TFOOT, FEMORPHOLOGY, COL, @@ -1533,14 +1537,14 @@ public void destructor() { VIDEO, BR, FOOTER, -TR, -DETAILS, -DT, -FOREIGNOBJECT, -FESPOTLIGHT, -INPUT, -RT, -TT, +ADDRESS, +MS, +APPLET, +FIELDSET, +FEPOINTLIGHT, +LINEARGRADIENT, +OBJECT, +RECT, SLOT, MENU, FECONVOLVEMATRIX, @@ -1585,23 +1589,23 @@ public void destructor() { ANIMATECOLOR, FECOMPONENTTRANSFER, HEADER, -NOBR, -ADDRESS, -DEFS, -MS, -PROGRESS, -APPLET, -DATALIST, -FIELDSET, -FEOFFSET, -FEPOINTLIGHT, -FONT, -LINEARGRADIENT, -NOSCRIPT, -OBJECT, -PLAINTEXT, -RECT, -SELECT, +TR, +CANVAS, +DETAILS, +NOFRAMES, +DT, +BASEFONT, +FOREIGNOBJECT, +FRAMESET, +FESPOTLIGHT, +FEDISTANTLIGHT, +INPUT, +MTEXT, +RT, +OUTPUT, +TT, +RADIALGRADIENT, +SELECTEDCONTENT, SCRIPT, TEXT, FEDROPSHADOW, @@ -1689,22 +1693,23 @@ public void destructor() { FILTER, FEGAUSSIANBLUR, MARKER, +NOBR, }; private final static int[] ELEMENT_HASHES = { 1900845386, 1748359220, -2001349720, +2001349736, 876609538, 1798686984, 1971465813, -2007781534, +2008125638, 59768833, 1730965751, 1756474198, 1864368130, 1938817026, -1988763672, -2005324101, +1990037800, +2005719336, 2060065124, 52490899, 62390273, @@ -1716,10 +1721,10 @@ public void destructor() { 1881498736, 1907661127, 1967128578, -1982935782, -1999397992, -2001392798, -2006329158, +1983533124, +2000525512, +2001495140, +2006896969, 2008851557, 2085266636, 51961587, @@ -1742,14 +1747,14 @@ public void destructor() { 1925844629, 1963982850, 1967795958, -1973420034, -1983633431, -1998585858, -2001309869, -2001392795, -2003183333, -2005925890, -2006974466, +1982173479, +1986527234, +1998724870, +2001349704, +2001392796, +2004635806, +2006028454, +2007601444, 2008325940, 2021937364, 2068523856, @@ -1794,23 +1799,23 @@ public void destructor() { 1965334268, 1967788867, 1968836118, -1971938532, -1982173479, -1983533124, -1986527234, -1990037800, -1998724870, -2000525512, -2001349704, -2001349736, -2001392796, -2001495140, -2004635806, -2005719336, -2006028454, -2006896969, -2007601444, -2008125638, +1973420034, +1982935782, +1983633431, +1988763672, +1998585858, +1999397992, +2001309869, +2001349720, +2001392795, +2001392798, +2003183333, +2005324101, +2005925890, +2006329158, +2006974466, +2007781534, +2008305999, 2008340774, 2008994116, 2051837468, @@ -1898,5 +1903,6 @@ public void destructor() { 1967795910, 1968053806, 1971461414, +1971938532, }; } diff --git a/src/nu/validator/htmlparser/impl/TreeBuilder.java b/src/nu/validator/htmlparser/impl/TreeBuilder.java index ba539f9c..b0999ecf 100644 --- a/src/nu/validator/htmlparser/impl/TreeBuilder.java +++ b/src/nu/validator/htmlparser/impl/TreeBuilder.java @@ -226,12 +226,6 @@ public abstract class TreeBuilder implements TokenHandler, // no fall-through - private static final int IN_SELECT_IN_TABLE = 10; - - private static final int IN_SELECT = 11; - - // no fall-through - private static final int AFTER_BODY = 12; // no fall-through @@ -952,9 +946,6 @@ public final void characters(@Const @NoLength char[] buf, int start, int length) * current node. */ break charactersloop; - case IN_SELECT: - case IN_SELECT_IN_TABLE: - break charactersloop; case IN_TABLE: case IN_TABLE_BODY: case IN_ROW: @@ -1166,9 +1157,6 @@ public final void characters(@Const @NoLength char[] buf, int start, int length) mode = IN_TABLE; i--; continue; - case IN_SELECT: - case IN_SELECT_IN_TABLE: - break charactersloop; case AFTER_BODY: errNonSpaceAfterBody(); fatal(); @@ -1334,8 +1322,6 @@ public final void eof() throws SAXException { case IN_TABLE_BODY: case IN_ROW: case IN_TABLE: - case IN_SELECT_IN_TABLE: - case IN_SELECT: case IN_COLUMN_GROUP: case FRAMESET_OK: case IN_CAPTION: @@ -2170,6 +2156,33 @@ public final void startTag(ElementName elementName, break starttagloop; case HR: implicitlyCloseP(); + // https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inbody + // "A start tag whose tag name is "hr"" + // "If the stack of open elements has a select element in scope:" + if (findLastInScope("select") != TreeBuilder.NOT_FOUND_ON_STACK) { + // "1. Generate implied end tags." + generateImpliedEndTags(); + // "2. If the stack of open elements has an option element + // in scope or has an optgroup element in scope, then + // this is a parse error." + if (errorHandler != null + && (findLastInScope("option") != TreeBuilder.NOT_FOUND_ON_STACK + || findLastInScope("optgroup") != TreeBuilder.NOT_FOUND_ON_STACK)) { + errUnclosedElementsImplied( + findLastInScope("option") != TreeBuilder.NOT_FOUND_ON_STACK + ? findLastInScope("option") + : findLastInScope("optgroup"), + name); + } + // "3. Insert an HTML element for the token." + appendVoidElementToCurrent(elementName, attributes); + selfClosing = false; + // [NOCPP[ + voidElement = true; + // ]NOCPP] + attributes = null; // CPP + break starttagloop; + } appendVoidElementToCurrentMayFoster( elementName, attributes); @@ -2184,7 +2197,32 @@ public final void startTag(ElementName elementName, elementName = ElementName.IMG; continue starttagloop; case IMG: + reconstructTheActiveFormattingElements(); + appendVoidElementToCurrentMayFoster( + elementName, attributes, + formPointer); + selfClosing = false; + // [NOCPP[ + voidElement = true; + // ]NOCPP] + attributes = null; // CPP + break starttagloop; case INPUT: + // https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inbody + // "A start tag whose tag name is "input"" + // "If the stack of open elements has a select element + // in scope:" + eltPos = findLastInScope("select"); + if (eltPos != TreeBuilder.NOT_FOUND_ON_STACK) { + // "Parse error." + errStartTagWithSelectOpen(name); + // "Pop elements until a select element has been popped." + while (currentPtr >= eltPos) { + pop(); + } + resetTheInsertionMode(); + continue starttagloop; + } reconstructTheActiveFormattingElements(); appendVoidElementToCurrentMayFoster( elementName, attributes, @@ -2235,31 +2273,97 @@ public final void startTag(ElementName elementName, attributes = null; // CPP break starttagloop; case SELECT: + // https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inbody + // "A start tag whose tag name is "select"" + // "If the parser was created as part of the HTML fragment + // parsing algorithm and the context element is a select + // element:" + if (fragment && "select" == contextName) { + // "Parse error. Ignore the token." + errStartSelectWhereEndSelectExpected(); + break starttagloop; + } + // "Otherwise, if the stack of open elements has a select + // element in scope:" + eltPos = findLastInScope(name); + if (eltPos != TreeBuilder.NOT_FOUND_ON_STACK) { + // "Parse error." + errStartSelectWhereEndSelectExpected(); + // "Pop elements until a select element has been popped." + while (currentPtr >= eltPos) { + pop(); + } + resetTheInsertionMode(); + break starttagloop; + } + // "Otherwise:" + // "Reconstruct the active formatting elements, if any." reconstructTheActiveFormattingElements(); + // "Insert an HTML element for the token." appendToCurrentNodeAndPushElementMayFoster( elementName, attributes, formPointer); - switch (mode) { - case IN_TABLE: - case IN_CAPTION: - case IN_COLUMN_GROUP: - case IN_TABLE_BODY: - case IN_ROW: - case IN_CELL: - mode = IN_SELECT_IN_TABLE; - break; - default: - mode = IN_SELECT; - break; + // "Set the frameset-ok flag to "not ok"." + framesetOk = false; + attributes = null; // CPP + break starttagloop; + case OPTION: + // https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inbody + // "A start tag whose tag name is "option"" + // "If the stack of open elements has a select element in scope:" + if (findLastInScope("select") != TreeBuilder.NOT_FOUND_ON_STACK) { + // "1. Generate implied end tags except for optgroup elements." + generateImpliedEndTagsExceptFor("optgroup"); + // "2. If the stack of open elements has an option element + // in scope, then this is a parse error." + if (errorHandler != null + && findLastInScope("option") != TreeBuilder.NOT_FOUND_ON_STACK) { + errUnclosedElementsImplied(findLastInScope("option"), name); + } + } else { + // "Otherwise, if the current node is an option element, + // then pop the current node from the stack of open elements." + if (isCurrent("option")) { + pop(); + } } + // "Reconstruct the active formatting elements, if any." + reconstructTheActiveFormattingElements(); + // "Insert an HTML element for the token." + appendToCurrentNodeAndPushElementMayFoster( + elementName, + attributes); attributes = null; // CPP break starttagloop; case OPTGROUP: - case OPTION: - if (isCurrent("option")) { - pop(); + // https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inbody + // "A start tag whose tag name is "optgroup"" + // "If the stack of open elements has a select element in scope:" + if (findLastInScope("select") != TreeBuilder.NOT_FOUND_ON_STACK) { + // "1. Generate implied end tags." + generateImpliedEndTags(); + // "2. If the stack of open elements has an option element + // in scope or has an optgroup element in scope, then + // this is a parse error." + if (errorHandler != null + && (findLastInScope("option") != TreeBuilder.NOT_FOUND_ON_STACK + || findLastInScope("optgroup") != TreeBuilder.NOT_FOUND_ON_STACK)) { + if (findLastInScope("option") != TreeBuilder.NOT_FOUND_ON_STACK) { + errUnclosedElementsImplied(findLastInScope("option"), name); + } else { + errUnclosedElementsImplied(findLastInScope("optgroup"), name); + } + } + } else { + // "Otherwise, if the current node is an option element, + // then pop the current node from the stack of open elements." + if (isCurrent("option")) { + pop(); + } } + // "Reconstruct the active formatting elements, if any." reconstructTheActiveFormattingElements(); + // "Insert an HTML element for the token." appendToCurrentNodeAndPushElementMayFoster( elementName, attributes); @@ -2329,14 +2433,18 @@ public final void startTag(ElementName elementName, attributes = null; // CPP break starttagloop; case CAPTION: - case COL: - case COLGROUP: case TBODY_OR_THEAD_OR_TFOOT: case TR: case TD_OR_TH: + case COL: + case COLGROUP: case FRAME: case FRAMESET: case HEAD: + // https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inbody + // "A start tag whose tag name is one of: "caption", "col", + // "colgroup", "frame", "frameset", "head", "tbody", "td", + // "tfoot", "th", "thead", "tr"" errStrayStartTag(name); break starttagloop; case OUTPUT: @@ -2514,111 +2622,6 @@ public final void startTag(ElementName elementName, mode = IN_TABLE; continue; } - case IN_SELECT_IN_TABLE: - switch (group) { - case CAPTION: - case TBODY_OR_THEAD_OR_TFOOT: - case TR: - case TD_OR_TH: - case TABLE: - errStartTagWithSelectOpen(name); - eltPos = findLastInTableScope("select"); - if (eltPos == TreeBuilder.NOT_FOUND_ON_STACK) { - assert fragment; - break starttagloop; // http://www.w3.org/Bugs/Public/show_bug.cgi?id=8375 - } - while (currentPtr >= eltPos) { - pop(); - } - resetTheInsertionMode(); - continue; - default: - // fall through to IN_SELECT - } - // CPPONLY: MOZ_FALLTHROUGH; - case IN_SELECT: - switch (group) { - case HTML: - errStrayStartTag(name); - if (!fragment) { - addAttributesToHtml(attributes); - attributes = null; // CPP - } - break starttagloop; - case OPTION: - if (isCurrent("option")) { - pop(); - } - appendToCurrentNodeAndPushElement( - elementName, - attributes); - attributes = null; // CPP - break starttagloop; - case OPTGROUP: - if (isCurrent("option")) { - pop(); - } - if (isCurrent("optgroup")) { - pop(); - } - appendToCurrentNodeAndPushElement( - elementName, - attributes); - attributes = null; // CPP - break starttagloop; - case SELECT: - errStartSelectWhereEndSelectExpected(); - eltPos = findLastInTableScope(name); - if (eltPos == TreeBuilder.NOT_FOUND_ON_STACK) { - assert fragment; - errNoSelectInTableScope(); - break starttagloop; - } else { - while (currentPtr >= eltPos) { - pop(); - } - resetTheInsertionMode(); - break starttagloop; - } - case INPUT: - case TEXTAREA: - errStartTagWithSelectOpen(name); - eltPos = findLastInTableScope("select"); - if (eltPos == TreeBuilder.NOT_FOUND_ON_STACK) { - assert fragment; - break starttagloop; - } - while (currentPtr >= eltPos) { - pop(); - } - resetTheInsertionMode(); - continue; - case SCRIPT: - startTagScriptInHead(elementName, attributes); - attributes = null; // CPP - break starttagloop; - case TEMPLATE: - startTagTemplateInHead(elementName, attributes); - attributes = null; // CPP - break starttagloop; - case HR: - if (isCurrent("option")) { - pop(); - } - if (isCurrent("optgroup")) { - pop(); - } - appendVoidElementToCurrent(elementName, attributes); - selfClosing = false; - // [NOCPP[ - voidElement = true; - // ]NOCPP] - attributes = null; // CPP - break starttagloop; - default: - errStrayStartTag(name); - break starttagloop; - } case AFTER_BODY: switch (group) { case HTML: @@ -3525,7 +3528,11 @@ public final void endTag(ElementName elementName) throws SAXException { case PRE_OR_LISTING: case FIELDSET: case BUTTON: + case SELECT: case ADDRESS_OR_ARTICLE_OR_ASIDE_OR_DETAILS_OR_DIALOG_OR_DIR_OR_FIGCAPTION_OR_FIGURE_OR_FOOTER_OR_HEADER_OR_HGROUP_OR_MAIN_OR_NAV_OR_SEARCH_OR_SECTION_OR_SUMMARY: + // https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inbody + // "An end tag whose tag name is one of: "address", "article", + // ..., "select", ..., "ul"" eltPos = findLastInScope(name); if (eltPos == TreeBuilder.NOT_FOUND_ON_STACK) { errStrayEndTag(name); @@ -3685,7 +3692,6 @@ public final void endTag(ElementName elementName) throws SAXException { case IFRAME: case NOEMBED: // XXX??? case NOFRAMES: // XXX?? - case SELECT: case TABLE: case TEXTAREA: // XXX?? errStrayEndTag(name); @@ -3795,72 +3801,6 @@ public final void endTag(ElementName elementName) throws SAXException { mode = IN_TABLE; continue; } - case IN_SELECT_IN_TABLE: - switch (group) { - case CAPTION: - case TABLE: - case TBODY_OR_THEAD_OR_TFOOT: - case TR: - case TD_OR_TH: - errEndTagSeenWithSelectOpen(name); - if (findLastInTableScope(name) != TreeBuilder.NOT_FOUND_ON_STACK) { - eltPos = findLastInTableScope("select"); - if (eltPos == TreeBuilder.NOT_FOUND_ON_STACK) { - assert fragment; - break endtagloop; // http://www.w3.org/Bugs/Public/show_bug.cgi?id=8375 - } - while (currentPtr >= eltPos) { - pop(); - } - resetTheInsertionMode(); - continue; - } else { - break endtagloop; - } - default: - // fall through to IN_SELECT - } - // CPPONLY: MOZ_FALLTHROUGH; - case IN_SELECT: - switch (group) { - case OPTION: - if (isCurrent("option")) { - pop(); - break endtagloop; - } else { - errStrayEndTag(name); - break endtagloop; - } - case OPTGROUP: - if (isCurrent("option") - && "optgroup" == stack[currentPtr - 1].name) { - pop(); - } - if (isCurrent("optgroup")) { - pop(); - } else { - errStrayEndTag(name); - } - break endtagloop; - case SELECT: - eltPos = findLastInTableScope("select"); - if (eltPos == TreeBuilder.NOT_FOUND_ON_STACK) { - assert fragment; - errStrayEndTag(name); - break endtagloop; - } - while (currentPtr >= eltPos) { - pop(); - } - resetTheInsertionMode(); - break endtagloop; - case TEMPLATE: - endTagTemplateInHead(); - break endtagloop; - default: - errStrayEndTag(name); - break endtagloop; - } case AFTER_BODY: switch (group) { case HTML: @@ -4320,23 +4260,7 @@ private void resetTheInsertionMode() { return; } } - if ("select" == name) { - int ancestorIndex = i; - while (ancestorIndex > 0) { - StackNode ancestor = stack[ancestorIndex--]; - if ("http://www.w3.org/1999/xhtml" == ancestor.ns) { - if ("template" == ancestor.name) { - break; - } - if ("table" == ancestor.name) { - mode = IN_SELECT_IN_TABLE; - return; - } - } - } - mode = IN_SELECT; - return; - } else if ("td" == name || "th" == name) { + if ("td" == name || "th" == name) { mode = IN_CELL; return; } else if ("tr" == name) { @@ -5097,6 +5021,9 @@ private void popTemplateMode() { private void pop() throws SAXException { StackNode node = stack[currentPtr]; assert debugOnlyClearLastStackSlot(); + if (node.getGroup() == OPTION) { + optionElementPopped(node.node); + } currentPtr--; elementPopped(node.ns, node.popName, node.node); node.release(this); @@ -5108,6 +5035,9 @@ private void popForeign(int origPos, int eltPos) throws SAXException { markMalformedIfScript(node.node); } assert debugOnlyClearLastStackSlot(); + if (node.getGroup() == OPTION) { + optionElementPopped(node.node); + } currentPtr--; elementPopped(node.ns, node.popName, node.node); node.release(this); @@ -5116,6 +5046,9 @@ private void popForeign(int origPos, int eltPos) throws SAXException { private void silentPop() throws SAXException { StackNode node = stack[currentPtr]; assert debugOnlyClearLastStackSlot(); + if (node.getGroup() == OPTION) { + optionElementPopped(node.node); + } currentPtr--; node.release(this); } @@ -5123,6 +5056,9 @@ private void silentPop() throws SAXException { private void popOnEof() throws SAXException { StackNode node = stack[currentPtr]; assert debugOnlyClearLastStackSlot(); + if (node.getGroup() == OPTION) { + optionElementPopped(node.node); + } currentPtr--; markMalformedIfScript(node.node); elementPopped(node.ns, node.popName, node.node); @@ -5760,6 +5696,18 @@ protected abstract T createHtmlElementSetAsRoot(HtmlAttributes attributes) protected abstract void detachFromParent(T element) throws SAXException; + /** + * Called when an option element is popped from the stack. + * + * https://html.spec.whatwg.org/multipage/form-elements.html#concept-select-option-selectedness + * Implements "maybe clone an option into selectedcontent" for + * customizable select. Subclasses that support DOM operations + * should override this to perform the cloning. + */ + protected void optionElementPopped(T option) throws SAXException { + // Default: no-op (streaming/SAX mode ignores cloning) + } + protected abstract boolean hasChildren(T element) throws SAXException; protected abstract void appendElement(T child, T newParent) @@ -6507,10 +6455,6 @@ private void errStartTagWithoutDoctype() throws SAXException { } } - private void errNoSelectInTableScope() throws SAXException { - err("No \u201Cselect\u201D in table scope."); - } - private void errStartSelectWhereEndSelectExpected() throws SAXException { err("\u201Cselect\u201D start tag where end tag expected."); } @@ -6588,14 +6532,6 @@ private void errEndTagAfterBody() throws SAXException { err("Saw an end tag after \u201Cbody\u201D had been closed."); } - private void errEndTagSeenWithSelectOpen(@Local String name) throws SAXException { - if (errorHandler == null) { - return; - } - errNoCheck("\u201C" + name - + "\u201D end tag with \u201Cselect\u201D open."); - } - private void errGarbageInColgroup() throws SAXException { err("Garbage in \u201Ccolgroup\u201D fragment."); } diff --git a/src/nu/validator/htmlparser/sax/SAXTreeBuilder.java b/src/nu/validator/htmlparser/sax/SAXTreeBuilder.java index a085ec8d..45131311 100644 --- a/src/nu/validator/htmlparser/sax/SAXTreeBuilder.java +++ b/src/nu/validator/htmlparser/sax/SAXTreeBuilder.java @@ -34,6 +34,7 @@ import nu.validator.saxtree.DocumentFragment; import nu.validator.saxtree.Element; import nu.validator.saxtree.Node; +import nu.validator.saxtree.NodeType; import nu.validator.saxtree.ParentNode; class SAXTreeBuilder extends TreeBuilder { @@ -197,4 +198,111 @@ private Node previousSibling(Node table) { throws SAXException { element.detach(); } + + @Override + // https://html.spec.whatwg.org/multipage/form-elements.html#concept-select-option-selectedness + // Implements "maybe clone an option into selectedcontent" + protected void optionElementPopped(Element option) throws SAXException { + // Find the nearest ancestor + Element selectedContent = findDescendant(select, "selectedcontent"); + if (selectedContent == null) { + return; + } + + // Check option selectedness + boolean hasSelected = option.getAttributes().getIndex("", "selected") >= 0; + if (!hasSelected && selectedContent.getFirstChild() != null) { + // Not the first option and no explicit selected attr + return; + } + + // Clear selectedcontent children and deep-clone option children + selectedContent.clearChildren(); + deepCloneChildren(option, selectedContent); + } + + private Element findAncestor(Element element, String localName) { + ParentNode parent = element.getParentNode(); + while (parent != null) { + if (parent.getNodeType() == NodeType.ELEMENT) { + Element elt = (Element) parent; + if (localName.equals(elt.getLocalName()) + && "http://www.w3.org/1999/xhtml".equals(elt.getUri())) { + return elt; + } + } + if (parent instanceof Node) { + parent = ((Node) parent).getParentNode(); + } else { + break; + } + } + return null; + } + + private Element findDescendant(Element root, String localName) { + Node child = root.getFirstChild(); + while (child != null) { + if (child.getNodeType() == NodeType.ELEMENT) { + Element elt = (Element) child; + if (localName.equals(elt.getLocalName()) + && "http://www.w3.org/1999/xhtml".equals(elt.getUri())) { + return elt; + } + Element found = findDescendant(elt, localName); + if (found != null) { + return found; + } + } + child = child.getNextSibling(); + } + return null; + } + + private void deepCloneChildren(Element source, Element destination) throws SAXException { + Node child = source.getFirstChild(); + while (child != null) { + deepCloneNode(child, destination); + child = child.getNextSibling(); + } + } + + private void deepCloneNode(Node node, ParentNode destination) throws SAXException { + switch (node.getNodeType()) { + case ELEMENT: + Element srcElem = (Element) node; + // Create a clone element with copied attributes + Element cloneElem = new Element(null, + srcElem.getUri(), + srcElem.getLocalName(), + srcElem.getQName(), + srcElem.getAttributes(), + false, // copy attributes + srcElem.getPrefixMappings()); + destination.appendChild(cloneElem); + // Recursively clone children + Node child = srcElem.getFirstChild(); + while (child != null) { + deepCloneNode(child, cloneElem); + child = child.getNextSibling(); + } + break; + case CHARACTERS: + // Clone the characters + Characters srcChars = (Characters) node; + char[] buf = srcChars.getBuffer(); + Characters cloneChars = new Characters(null, buf, 0, buf.length); + destination.appendChild(cloneChars); + break; + default: + // Ignore other node types for now + break; + } + } } diff --git a/src/nu/validator/htmlparser/xom/XOMTreeBuilder.java b/src/nu/validator/htmlparser/xom/XOMTreeBuilder.java index 635fc9ff..1e3301c1 100644 --- a/src/nu/validator/htmlparser/xom/XOMTreeBuilder.java +++ b/src/nu/validator/htmlparser/xom/XOMTreeBuilder.java @@ -348,4 +348,70 @@ private int indexOfTable(Element table, Element stackParent) { cachedTableIndex = -1; cachedTable = null; } + + @Override + // https://html.spec.whatwg.org/multipage/form-elements.html#concept-select-option-selectedness + // Implements "maybe clone an option into selectedcontent" + protected void optionElementPopped(Element option) throws SAXException { + try { + // Find the nearest ancestor + Element selectedContent = findSelectedContent(select); + if (selectedContent == null) { + return; + } + + // Check option selectedness + boolean hasSelected = option.getAttribute("selected") != null; + if (!hasSelected && selectedContent.getChildCount() > 0) { + // Not the first option and no explicit selected attr + return; + } + + // Clear selectedcontent children and deep-clone option children + selectedContent.removeChildren(); + for (int i = 0; i < option.getChildCount(); i++) { + selectedContent.appendChild(option.getChild(i).copy()); + } + } catch (XMLException e) { + fatal(e); + } + } + + private Element findSelectedContent(Element root) { + for (int i = 0; i < root.getChildCount(); i++) { + Node child = root.getChild(i); + if (child instanceof Element) { + Element elt = (Element) child; + if ("selectedcontent".equals(elt.getLocalName()) + && "http://www.w3.org/1999/xhtml".equals( + elt.getNamespaceURI())) { + return elt; + } + Element found = findSelectedContent(elt); + if (found != null) { + return found; + } + } + } + return null; + } } diff --git a/src/nu/validator/saxtree/CharBufferNode.java b/src/nu/validator/saxtree/CharBufferNode.java index 55c7715f..71ebf2f4 100644 --- a/src/nu/validator/saxtree/CharBufferNode.java +++ b/src/nu/validator/saxtree/CharBufferNode.java @@ -50,6 +50,14 @@ public abstract class CharBufferNode extends Node { System.arraycopy(buf, start, buffer, 0, length); } + /** + * Returns the buffer. + * @return the buffer + */ + public char[] getBuffer() { + return buffer; + } + /** * Returns the wrapped buffer as a string. * diff --git a/src/nu/validator/saxtree/ParentNode.java b/src/nu/validator/saxtree/ParentNode.java index 6cc96003..b72acee9 100644 --- a/src/nu/validator/saxtree/ParentNode.java +++ b/src/nu/validator/saxtree/ParentNode.java @@ -202,7 +202,22 @@ void removeChild(Node node) { prev.setNextSibling(node.getNextSibling()); if (lastChild == node) { lastChild = prev; - } + } + } + } + + /** + * Remove all children from this node. + */ + public void clearChildren() { + Node child = firstChild; + while (child != null) { + Node next = child.getNextSibling(); + child.setParentNode(null); + child.setNextSibling(null); + child = next; } + firstChild = null; + lastChild = null; } }