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; } }