diff --git a/gwt-src/nu/validator/htmlparser/gwt/BrowserTreeBuilder.java b/gwt-src/nu/validator/htmlparser/gwt/BrowserTreeBuilder.java
index 2eaa6764..377150a3 100644
--- a/gwt-src/nu/validator/htmlparser/gwt/BrowserTreeBuilder.java
+++ b/gwt-src/nu/validator/htmlparser/gwt/BrowserTreeBuilder.java
@@ -474,4 +474,21 @@ private static native void removeChild(JavaScriptObject parent,
fatal(e);
}
}
+
+ @Override
+ protected void cloneOptionContentToSelectedContent(
+ JavaScriptObject option, JavaScriptObject selectedContent)
+ throws SAXException {
+ try {
+ while (hasChildNodes(selectedContent)) {
+ removeChild(selectedContent, getFirstChild(selectedContent));
+ }
+ JavaScriptObject clone = cloneNodeDeep(option);
+ while (hasChildNodes(clone)) {
+ appendChild(selectedContent, getFirstChild(clone));
+ }
+ } catch (JavaScriptException e) {
+ fatal(e);
+ }
+ }
}
diff --git a/src/nu/validator/htmlparser/dom/DOMTreeBuilder.java b/src/nu/validator/htmlparser/dom/DOMTreeBuilder.java
index 7470a151..c0fb6e2b 100644
--- a/src/nu/validator/htmlparser/dom/DOMTreeBuilder.java
+++ b/src/nu/validator/htmlparser/dom/DOMTreeBuilder.java
@@ -354,4 +354,21 @@ protected Element createAndInsertFosterParentedElement(String ns, String name,
fatal(e);
}
}
+
+ @Override
+ protected void cloneOptionContentToSelectedContent(Element option,
+ Element selectedContent) throws SAXException {
+ try {
+ while (selectedContent.hasChildNodes()) {
+ selectedContent.removeChild(selectedContent.getFirstChild());
+ }
+ Node child = option.getFirstChild();
+ while (child != null) {
+ selectedContent.appendChild(child.cloneNode(true));
+ child = child.getNextSibling();
+ }
+ } catch (DOMException e) {
+ fatal(e);
+ }
+ }
}
diff --git a/src/nu/validator/htmlparser/impl/ElementName.java b/src/nu/validator/htmlparser/impl/ElementName.java
index 2d09c338..9b9f3ba8 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.SELECTEDCONTENT);
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..9a51d331 100644
--- a/src/nu/validator/htmlparser/impl/TreeBuilder.java
+++ b/src/nu/validator/htmlparser/impl/TreeBuilder.java
@@ -200,6 +200,8 @@ public abstract class TreeBuilder implements TokenHandler,
final static int IMG = 67;
+ final static int SELECTEDCONTENT = 68;
+
// start insertion modes
private static final int IN_ROW = 0;
@@ -426,6 +428,21 @@ public abstract class TreeBuilder implements TokenHandler,
private T headPointer;
+ // For customizable select: tracks the selectedcontent element to clone option content into
+ protected T selectedContentPointer;
+
+ // Tracks the position in stack where selectedcontent was found
+ private int selectedContentStackPos = -1;
+
+ // Tracks if we're inside an option that should have its content cloned to selectedcontent
+ private int activeOptionStackPos = -1;
+
+ // Tracks if we've seen an option with the 'selected' attribute in the current select
+ private boolean seenSelectedOption = false;
+
+ // Tracks if we've already had an active option (first option was selected for cloning)
+ private boolean hadActiveOption = false;
+
protected @Auto char[] charBuffer;
protected int charBufferLen = 0;
@@ -606,6 +623,11 @@ public boolean dropBufferIfLongerThan(int length) {
listPtr = -1;
formPointer = null;
headPointer = null;
+ selectedContentPointer = null;
+ selectedContentStackPos = -1;
+ activeOptionStackPos = -1;
+ seenSelectedOption = false;
+ hadActiveOption = false;
// [NOCPP[
idLocations.clear();
wantingComments = wantsComments();
@@ -1436,6 +1458,11 @@ public final void eof() throws SAXException {
public final void endTokenization() throws SAXException {
formPointer = null;
headPointer = null;
+ selectedContentPointer = null;
+ selectedContentStackPos = -1;
+ activeOptionStackPos = -1;
+ seenSelectedOption = false;
+ hadActiveOption = false;
contextName = null;
contextNode = null;
templateModeStack = null;
@@ -2169,6 +2196,23 @@ public final void startTag(ElementName elementName,
attributes = null; // CPP
break starttagloop;
case HR:
+ // Check if select is in scope
+ if (findLastInScope("select") != TreeBuilder.NOT_FOUND_ON_STACK) {
+ // Close any open option or optgroup first
+ if (isCurrent("option")) {
+ pop();
+ }
+ if (isCurrent("optgroup")) {
+ pop();
+ }
+ appendVoidElementToCurrent(elementName, attributes);
+ selfClosing = false;
+ // [NOCPP[
+ voidElement = true;
+ // ]NOCPP]
+ attributes = null; // CPP
+ break starttagloop;
+ }
implicitlyCloseP();
appendVoidElementToCurrentMayFoster(
elementName,
@@ -2184,7 +2228,27 @@ 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:
+ // Check if select is in scope and close it (for compatibility)
+ eltPos = findLastInScope("select");
+ if (eltPos != TreeBuilder.NOT_FOUND_ON_STACK) {
+ errStartTagWithSelectOpen(name);
+ while (currentPtr >= eltPos) {
+ pop();
+ }
+ resetTheInsertionMode();
+ continue starttagloop;
+ }
reconstructTheActiveFormattingElements();
appendVoidElementToCurrentMayFoster(
elementName, attributes,
@@ -2196,6 +2260,16 @@ public final void startTag(ElementName elementName,
attributes = null; // CPP
break starttagloop;
case TEXTAREA:
+ // Check if select is in scope and close it (for compatibility)
+ eltPos = findLastInScope("select");
+ if (eltPos != TreeBuilder.NOT_FOUND_ON_STACK) {
+ errStartTagWithSelectOpen(name);
+ while (currentPtr >= eltPos) {
+ pop();
+ }
+ resetTheInsertionMode();
+ continue starttagloop;
+ }
appendToCurrentNodeAndPushElementMayFoster(
elementName,
attributes, formPointer);
@@ -2235,27 +2309,103 @@ public final void startTag(ElementName elementName,
attributes = null; // CPP
break starttagloop;
case SELECT:
+ // Check if select is already in scope (nested select)
+ eltPos = findLastInScope(name);
+ if (eltPos != TreeBuilder.NOT_FOUND_ON_STACK) {
+ // Nested select acts like - close the existing one
+ // but do NOT insert a new select element
+ errStartSelectWhereEndSelectExpected();
+ generateImpliedEndTags();
+ if (errorHandler != null
+ && !isCurrent(name)) {
+ errUnclosedElementsImplied(eltPos, name);
+ }
+ while (currentPtr >= eltPos) {
+ pop();
+ }
+ resetTheInsertionMode();
+ break starttagloop;
+ } else {
+ reconstructTheActiveFormattingElements();
+ appendToCurrentNodeAndPushElementMayFoster(
+ elementName,
+ attributes, formPointer);
+ // No longer switch to IN_SELECT mode
+ attributes = null; // CPP
+ break starttagloop;
+ }
+ case OPTION:
+ // Check if select is in scope
+ if (findLastInScope("select") != TreeBuilder.NOT_FOUND_ON_STACK) {
+ // Reconstruct active formatting elements first
+ reconstructTheActiveFormattingElements();
+ // Generate implied end tags except for optgroup
+ generateImpliedEndTagsExceptFor("optgroup");
+ // Check if option is in scope and close it
+ eltPos = findLastInScope("option");
+ if (eltPos != TreeBuilder.NOT_FOUND_ON_STACK) {
+ if (errorHandler != null && !isCurrent("option")) {
+ errUnclosedElementsImplied(eltPos, "option");
+ }
+ while (currentPtr >= eltPos) {
+ pop();
+ }
+ }
+ // Check if this option should be active for selectedcontent cloning
+ boolean hasSelected = attributes.contains(AttributeName.SELECTED);
+ boolean shouldBeActive = false;
+ if (selectedContentPointer != null) {
+ if (hasSelected) {
+ // Option with selected attr becomes active
+ seenSelectedOption = true;
+ shouldBeActive = true;
+ } else if (!seenSelectedOption && !hadActiveOption) {
+ // First option without selected - tentatively active
+ shouldBeActive = true;
+ }
+ }
+ appendToCurrentNodeAndPushElement(
+ elementName,
+ attributes);
+ if (shouldBeActive) {
+ activeOptionStackPos = currentPtr;
+ hadActiveOption = true;
+ }
+ attributes = null; // CPP
+ break starttagloop;
+ }
+ // Outside select, fall through to old behavior
+ if (isCurrent("option")) {
+ pop();
+ }
reconstructTheActiveFormattingElements();
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;
- }
+ attributes);
attributes = null; // CPP
break starttagloop;
case OPTGROUP:
- case OPTION:
+ // Check if select is in scope
+ if (findLastInScope("select") != TreeBuilder.NOT_FOUND_ON_STACK) {
+ // Generate implied end tags
+ generateImpliedEndTags();
+ // Check if optgroup is in scope and close it
+ eltPos = findLastInScope("optgroup");
+ if (eltPos != TreeBuilder.NOT_FOUND_ON_STACK) {
+ if (errorHandler != null && !isCurrent("optgroup")) {
+ errUnclosedElementsImplied(eltPos, "optgroup");
+ }
+ while (currentPtr >= eltPos) {
+ pop();
+ }
+ }
+ appendToCurrentNodeAndPushElement(
+ elementName,
+ attributes);
+ attributes = null; // CPP
+ break starttagloop;
+ }
+ // Outside select, fall through to old behavior
if (isCurrent("option")) {
pop();
}
@@ -2329,11 +2479,25 @@ 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:
+ // Check if we're inside a select inside a table
+ // If so, close the select and reprocess
+ if (findLastInScope("select") != TreeBuilder.NOT_FOUND_ON_STACK
+ && findLastInTableScope("table") != TreeBuilder.NOT_FOUND_ON_STACK) {
+ errStartTagWithSelectOpen(name);
+ eltPos = findLastInScope("select");
+ while (currentPtr >= eltPos) {
+ pop();
+ }
+ resetTheInsertionMode();
+ continue starttagloop;
+ }
+ errStrayStartTag(name);
+ break starttagloop;
+ case COL:
+ case COLGROUP:
case FRAME:
case FRAMESET:
case HEAD:
@@ -2346,6 +2510,26 @@ public final void startTag(ElementName elementName,
attributes, formPointer);
attributes = null; // CPP
break starttagloop;
+ case SELECTEDCONTENT:
+ // Track selectedcontent for cloning option content
+ if (findLastInScope("select") != TreeBuilder.NOT_FOUND_ON_STACK) {
+ reconstructTheActiveFormattingElements();
+ appendToCurrentNodeAndPushElement(
+ elementName,
+ attributes);
+ // Save pointer for content cloning
+ selectedContentPointer = stack[currentPtr].node;
+ selectedContentStackPos = currentPtr;
+ attributes = null; // CPP
+ break starttagloop;
+ }
+ // Outside select, treat as normal element
+ reconstructTheActiveFormattingElements();
+ appendToCurrentNodeAndPushElementMayFoster(
+ elementName,
+ attributes);
+ attributes = null; // CPP
+ break starttagloop;
default:
reconstructTheActiveFormattingElements();
appendToCurrentNodeAndPushElementMayFoster(
@@ -3627,6 +3811,51 @@ public final void endTag(ElementName elementName) throws SAXException {
}
}
break endtagloop;
+ case OPTION:
+ // Handle option end tag when select is in scope
+ if (findLastInScope("select") != TreeBuilder.NOT_FOUND_ON_STACK) {
+ eltPos = findLastInScope("option");
+ if (eltPos == TreeBuilder.NOT_FOUND_ON_STACK) {
+ errStrayEndTag(name);
+ } else {
+ generateImpliedEndTagsExceptFor("option");
+ if (errorHandler != null && !isCurrent("option")) {
+ errUnclosedElements(eltPos, name);
+ }
+ while (currentPtr >= eltPos) {
+ pop();
+ }
+ }
+ break endtagloop;
+ }
+ // Outside select, treat as stray end tag
+ errStrayEndTag(name);
+ break endtagloop;
+ case OPTGROUP:
+ // Handle optgroup end tag when select is in scope
+ if (findLastInScope("select") != TreeBuilder.NOT_FOUND_ON_STACK) {
+ // If current node is option and previous is optgroup, close option first
+ if (isCurrent("option") && currentPtr >= 1
+ && "optgroup" == stack[currentPtr - 1].name) {
+ pop();
+ }
+ eltPos = findLastInScope("optgroup");
+ if (eltPos == TreeBuilder.NOT_FOUND_ON_STACK) {
+ errStrayEndTag(name);
+ } else {
+ generateImpliedEndTagsExceptFor("optgroup");
+ if (errorHandler != null && !isCurrent("optgroup")) {
+ errUnclosedElements(eltPos, name);
+ }
+ while (currentPtr >= eltPos) {
+ pop();
+ }
+ }
+ break endtagloop;
+ }
+ // Outside select, treat as stray end tag
+ errStrayEndTag(name);
+ break endtagloop;
case H1_OR_H2_OR_H3_OR_H4_OR_H5_OR_H6:
eltPos = findLastInScopeHn();
if (eltPos == TreeBuilder.NOT_FOUND_ON_STACK) {
@@ -3674,6 +3903,22 @@ public final void endTag(ElementName elementName) throws SAXException {
case TEMPLATE:
// fall through to IN_HEAD;
break;
+ case SELECT:
+ // Handle select end tag when select is in scope
+ eltPos = findLastInScope("select");
+ if (eltPos == TreeBuilder.NOT_FOUND_ON_STACK) {
+ errStrayEndTag(name);
+ break endtagloop;
+ }
+ generateImpliedEndTags();
+ if (errorHandler != null && !isCurrent("select")) {
+ errUnclosedElements(eltPos, name);
+ }
+ while (currentPtr >= eltPos) {
+ pop();
+ }
+ resetTheInsertionMode();
+ break endtagloop;
case AREA_OR_WBR:
case KEYGEN: // XXX??
case PARAM_OR_SOURCE_OR_TRACK:
@@ -3685,7 +3930,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);
@@ -4321,20 +4565,9 @@ private void resetTheInsertionMode() {
}
}
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;
+ // With select parser relaxation, we no longer enter IN_SELECT mode
+ // Instead, stay in IN_BODY mode and handle select content there
+ mode = framesetOk ? FRAMESET_OK : IN_BODY;
return;
} else if ("td" == name || "th" == name) {
mode = IN_CELL;
@@ -5097,6 +5330,23 @@ private void popTemplateMode() {
private void pop() throws SAXException {
StackNode node = stack[currentPtr];
assert debugOnlyClearLastStackSlot();
+ // When active option closes, deep-clone its content to selectedcontent
+ // This handles adoption agency restructuring correctly
+ if (currentPtr == activeOptionStackPos) {
+ if (selectedContentPointer != null) {
+ cloneOptionContentToSelectedContent(node.node, selectedContentPointer);
+ }
+ activeOptionStackPos = -1;
+ }
+ // Clear selectedcontent tracking if we're popping the select element
+ // (not when popping selectedcontent itself - the DOM node is still valid)
+ if (node.getGroup() == SELECT) {
+ selectedContentPointer = null;
+ selectedContentStackPos = -1;
+ seenSelectedOption = false;
+ activeOptionStackPos = -1;
+ hadActiveOption = false;
+ }
currentPtr--;
elementPopped(node.ns, node.popName, node.node);
node.release(this);
@@ -5108,6 +5358,21 @@ private void popForeign(int origPos, int eltPos) throws SAXException {
markMalformedIfScript(node.node);
}
assert debugOnlyClearLastStackSlot();
+ // When active option closes, deep-clone its content to selectedcontent
+ if (currentPtr == activeOptionStackPos) {
+ if (selectedContentPointer != null) {
+ cloneOptionContentToSelectedContent(node.node, selectedContentPointer);
+ }
+ activeOptionStackPos = -1;
+ }
+ // Clear selectedcontent tracking if we're popping the select element
+ if (node.getGroup() == SELECT) {
+ selectedContentPointer = null;
+ selectedContentStackPos = -1;
+ seenSelectedOption = false;
+ activeOptionStackPos = -1;
+ hadActiveOption = false;
+ }
currentPtr--;
elementPopped(node.ns, node.popName, node.node);
node.release(this);
@@ -5116,6 +5381,21 @@ private void popForeign(int origPos, int eltPos) throws SAXException {
private void silentPop() throws SAXException {
StackNode node = stack[currentPtr];
assert debugOnlyClearLastStackSlot();
+ // When active option closes, deep-clone its content to selectedcontent
+ if (currentPtr == activeOptionStackPos) {
+ if (selectedContentPointer != null) {
+ cloneOptionContentToSelectedContent(node.node, selectedContentPointer);
+ }
+ activeOptionStackPos = -1;
+ }
+ // Clear selectedcontent tracking if we're popping the select element
+ if (node.getGroup() == SELECT) {
+ selectedContentPointer = null;
+ selectedContentStackPos = -1;
+ seenSelectedOption = false;
+ activeOptionStackPos = -1;
+ hadActiveOption = false;
+ }
currentPtr--;
node.release(this);
}
@@ -5123,6 +5403,21 @@ private void silentPop() throws SAXException {
private void popOnEof() throws SAXException {
StackNode node = stack[currentPtr];
assert debugOnlyClearLastStackSlot();
+ // When active option closes, deep-clone its content to selectedcontent
+ if (currentPtr == activeOptionStackPos) {
+ if (selectedContentPointer != null) {
+ cloneOptionContentToSelectedContent(node.node, selectedContentPointer);
+ }
+ activeOptionStackPos = -1;
+ }
+ // Clear selectedcontent tracking if we're popping the select element
+ if (node.getGroup() == SELECT) {
+ selectedContentPointer = null;
+ selectedContentStackPos = -1;
+ seenSelectedOption = false;
+ activeOptionStackPos = -1;
+ hadActiveOption = false;
+ }
currentPtr--;
markMalformedIfScript(node.node);
elementPopped(node.ns, node.popName, node.node);
@@ -5760,6 +6055,17 @@ protected abstract T createHtmlElementSetAsRoot(HtmlAttributes attributes)
protected abstract void detachFromParent(T element) throws SAXException;
+ /**
+ * Called when the active option is popped from the stack, to clone
+ * the option's children into selectedcontent. Subclasses that support
+ * DOM operations should override this to clear selectedcontent and
+ * deep-clone the option's children into it.
+ */
+ protected void cloneOptionContentToSelectedContent(T option, T selectedContent)
+ 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)
diff --git a/src/nu/validator/htmlparser/sax/SAXTreeBuilder.java b/src/nu/validator/htmlparser/sax/SAXTreeBuilder.java
index a085ec8d..ab027a0c 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,52 @@ private Node previousSibling(Node table) {
throws SAXException {
element.detach();
}
+
+ @Override
+ protected void cloneOptionContentToSelectedContent(Element option, Element selectedContent)
+ throws SAXException {
+ ((ParentNode) selectedContent).clearChildren();
+ deepCloneChildren(option, selectedContent);
+ }
+
+ 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..e760beb5 100644
--- a/src/nu/validator/htmlparser/xom/XOMTreeBuilder.java
+++ b/src/nu/validator/htmlparser/xom/XOMTreeBuilder.java
@@ -348,4 +348,17 @@ private int indexOfTable(Element table, Element stackParent) {
cachedTableIndex = -1;
cachedTable = null;
}
+
+ @Override
+ protected void cloneOptionContentToSelectedContent(Element option,
+ Element selectedContent) throws SAXException {
+ try {
+ selectedContent.removeChildren();
+ for (int i = 0; i < option.getChildCount(); i++) {
+ selectedContent.appendChild(option.getChild(i).copy());
+ }
+ } catch (XMLException e) {
+ fatal(e);
+ }
+ }
}
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;
}
}