Skip to content

Commit 8ad0683

Browse files
Align relaxed-<select> implementation with spec, in line with review comments
Move selectedcontent cloning from TreeBuilder to DOM-side subclasses: the optionElementPopped() hook now walks the live DOM to find the ancestor <select> and its <selectedcontent>, check selectedness, and clone option children — rather than relying on parser-tracked state. Implemented in all four subclasses (DOMTreeBuilder, SAXTreeBuilder, XOMTreeBuilder, BrowserTreeBuilder). Remove all dead IN_SELECT / IN_SELECT_IN_TABLE mode code (handlers, constants, end-tag special cases) — with <select> “relaxation”, those modes are never entered. Fix resetTheInsertionMode to skip past <select> elements on the stack instead of incorrectly returning IN_BODY — an enclosing td/th now correctly yields IN_CELL, an enclosing table yields IN_TABLE, etc. Align start-tag handling with the spec: - HR: use generateImpliedEndTags() instead of manual isCurrent/pop - OPTION/OPTGROUP: remove redundant pop loops (spec says parse error only), add missing reconstructTheActiveFormattingElements for optgroup, fix optgroup to check both option and optgroup in scope - SELECT: check fragment case first, remove generateImpliedEndTags from nested case, add framesetOk = false - Table elements (caption, tbody, tr, td, th): treat as stray start tags, removing the old IN_SELECT_IN_TABLE dual-scope check Fold </select> end tag into the standard block-element group (with div, fieldset, button, etc.) — the dedicated resetTheInsertionMode call is no longer needed. Add spec URLs and verbatim spec-text comments throughout all <select>-related code sections — for traceability and review-ability.
1 parent 87fe37a commit 8ad0683

6 files changed

Lines changed: 460 additions & 541 deletions

File tree

gwt-src/nu/validator/htmlparser/gwt/BrowserTreeBuilder.java

Lines changed: 82 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -475,20 +475,97 @@ private static native void removeChild(JavaScriptObject parent,
475475
}
476476
}
477477

478+
private static native JavaScriptObject getNextSibling(
479+
JavaScriptObject node) /*-{
480+
return node.nextSibling;
481+
}-*/;
482+
483+
private static native String getLocalName(
484+
JavaScriptObject node) /*-{
485+
return node.localName;
486+
}-*/;
487+
488+
private static native boolean hasAttribute(
489+
JavaScriptObject node, String name) /*-{
490+
return node.hasAttribute(name);
491+
}-*/;
492+
478493
@Override
479-
protected void cloneOptionContentToSelectedContent(
480-
JavaScriptObject option, JavaScriptObject selectedContent)
494+
// https://html.spec.whatwg.org/multipage/form-elements.html#maybe-clone-an-option-into-selectedcontent
495+
// Implements "maybe clone an option into selectedcontent"
496+
protected void optionElementPopped(JavaScriptObject option)
481497
throws SAXException {
482498
try {
499+
// Find the nearest ancestor <select> element
500+
JavaScriptObject ancestor = getParentNode(option);
501+
JavaScriptObject select = null;
502+
while (ancestor != null && getNodeType(ancestor) == 1) {
503+
if ("select".equals(getLocalName(ancestor))) {
504+
select = ancestor;
505+
break;
506+
}
507+
ancestor = getParentNode(ancestor);
508+
}
509+
if (select == null) {
510+
return;
511+
}
512+
if (hasAttribute(select, "multiple")) {
513+
return;
514+
}
515+
516+
// Find the first <selectedcontent> descendant of <select>
517+
JavaScriptObject selectedContent = findDescendantByLocalName(
518+
select, "selectedcontent");
519+
if (selectedContent == null) {
520+
return;
521+
}
522+
523+
// Check option selectedness
524+
boolean hasSelectedAttr = hasAttribute(option, "selected");
525+
if (!hasSelectedAttr && hasChildNodes(selectedContent)) {
526+
// Not the first option and no explicit selected attr
527+
return;
528+
}
529+
530+
// Clear selectedcontent children and deep-clone option children
483531
while (hasChildNodes(selectedContent)) {
484532
removeChild(selectedContent, getFirstChild(selectedContent));
485533
}
486-
JavaScriptObject clone = cloneNodeDeep(option);
487-
while (hasChildNodes(clone)) {
488-
appendChild(selectedContent, getFirstChild(clone));
534+
for (JavaScriptObject child = getFirstChild(option);
535+
child != null; child = getNextSibling(child)) {
536+
appendChild(selectedContent, cloneNodeDeep(child));
489537
}
490538
} catch (JavaScriptException e) {
491539
fatal(e);
492540
}
493541
}
542+
543+
private JavaScriptObject findDescendantByLocalName(
544+
JavaScriptObject root, String localName) {
545+
JavaScriptObject current = getFirstChild(root);
546+
if (current == null) {
547+
return null;
548+
}
549+
JavaScriptObject next;
550+
for (;;) {
551+
if (getNodeType(current) == 1
552+
&& localName.equals(getLocalName(current))) {
553+
return current;
554+
}
555+
if ((next = getFirstChild(current)) != null) {
556+
current = next;
557+
continue;
558+
}
559+
for (;;) {
560+
if (current == root) {
561+
return null;
562+
}
563+
if ((next = getNextSibling(current)) != null) {
564+
current = next;
565+
break;
566+
}
567+
current = getParentNode(current);
568+
}
569+
}
570+
}
494571
}

src/nu/validator/htmlparser/dom/DOMTreeBuilder.java

Lines changed: 73 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -356,19 +356,87 @@ protected Element createAndInsertFosterParentedElement(String ns, String name,
356356
}
357357

358358
@Override
359-
protected void cloneOptionContentToSelectedContent(Element option,
360-
Element selectedContent) throws SAXException {
359+
// https://html.spec.whatwg.org/multipage/form-elements.html#maybe-clone-an-option-into-selectedcontent
360+
// Implements "maybe clone an option into selectedcontent"
361+
protected void optionElementPopped(Element option) throws SAXException {
361362
try {
363+
// Find the nearest ancestor <select> element
364+
Node ancestor = option.getParentNode();
365+
Element select = null;
366+
while (ancestor != null) {
367+
if (ancestor.getNodeType() == Node.ELEMENT_NODE) {
368+
Element elt = (Element) ancestor;
369+
if ("select".equals(elt.getLocalName())
370+
&& "http://www.w3.org/1999/xhtml".equals(
371+
elt.getNamespaceURI())) {
372+
select = elt;
373+
break;
374+
}
375+
}
376+
ancestor = ancestor.getParentNode();
377+
}
378+
if (select == null) {
379+
return;
380+
}
381+
if (select.hasAttribute("multiple")) {
382+
return;
383+
}
384+
385+
// Find the first <selectedcontent> descendant of <select>
386+
Element selectedContent = findSelectedContent(select);
387+
if (selectedContent == null) {
388+
return;
389+
}
390+
391+
// Check option selectedness
392+
boolean hasSelected = option.hasAttribute("selected");
393+
if (!hasSelected && selectedContent.hasChildNodes()) {
394+
// Not the first option and no explicit selected attr
395+
return;
396+
}
397+
398+
// Clear selectedcontent children and deep-clone option children
362399
while (selectedContent.hasChildNodes()) {
363400
selectedContent.removeChild(selectedContent.getFirstChild());
364401
}
365-
Node child = option.getFirstChild();
366-
while (child != null) {
402+
for (Node child = option.getFirstChild(); child != null;
403+
child = child.getNextSibling()) {
367404
selectedContent.appendChild(child.cloneNode(true));
368-
child = child.getNextSibling();
369405
}
370406
} catch (DOMException e) {
371407
fatal(e);
372408
}
373409
}
410+
411+
private Element findSelectedContent(Element root) {
412+
Node current = root.getFirstChild();
413+
if (current == null) {
414+
return null;
415+
}
416+
Node next;
417+
for (;;) {
418+
if (current.getNodeType() == Node.ELEMENT_NODE) {
419+
Element elt = (Element) current;
420+
if ("selectedcontent".equals(elt.getLocalName())
421+
&& "http://www.w3.org/1999/xhtml".equals(
422+
elt.getNamespaceURI())) {
423+
return elt;
424+
}
425+
}
426+
if ((next = current.getFirstChild()) != null) {
427+
current = next;
428+
continue;
429+
}
430+
for (;;) {
431+
if (current == root) {
432+
return null;
433+
}
434+
if ((next = current.getNextSibling()) != null) {
435+
current = next;
436+
break;
437+
}
438+
current = current.getParentNode();
439+
}
440+
}
441+
}
374442
}

src/nu/validator/htmlparser/impl/ElementName.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1428,7 +1428,7 @@ public void destructor() {
14281428
public static final ElementName SELECTEDCONTENT = new ElementName("selectedcontent", "selectedcontent",
14291429
// CPPONLY: NS_NewHTMLElement,
14301430
// CPPONLY: NS_NewSVGUnknownElement,
1431-
TreeBuilder.SELECTEDCONTENT);
1431+
TreeBuilder.OTHER);
14321432
public static final ElementName SLOT = new ElementName("slot", "slot",
14331433
// CPPONLY: NS_NewHTMLSlotElement,
14341434
// CPPONLY: NS_NewSVGUnknownElement,

0 commit comments

Comments
 (0)