Skip to content

Commit e0ea675

Browse files
Replace inline cloning with option-pop hook for <selectedcontent>
The initial implementation did inline cloning during parsing (mirroring elements/characters into <selectedcontent> as tokens were processed) AND deep-cloned from the DOM when option was popped. The inline cloning was wrong per spec — cloning should operate on the DOM, not on what the parser saw (the adoption agency algorithm can restructure the tree) — and redundant, since pop() cleared <selectedcontent> and deep-cloned from the DOM anyway. This replaces all inline cloning machinery with a single overridable hook method, cloneOptionContentToSelectedContent(), called when <option> is popped. The tree builder tracks which <option> is active and where <selectedcontent> is, but delegates the actual DOM cloning to subclasses.
1 parent 4894df4 commit e0ea675

2 files changed

Lines changed: 16 additions & 142 deletions

File tree

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

Lines changed: 11 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535

3636
package nu.validator.htmlparser.impl;
3737

38-
import java.util.ArrayList;
3938
import java.util.Arrays;
4039
import java.util.HashMap;
4140
import java.util.Map;
@@ -444,10 +443,6 @@ public abstract class TreeBuilder<T> implements TokenHandler,
444443
// Tracks if we've already had an active option (first option was selected for cloning)
445444
private boolean hadActiveOption = false;
446445

447-
// Stack to track the current parent in selectedcontent for element cloning
448-
// When we're inside an active option, we push cloned elements here
449-
private ArrayList<T> selectedContentCloneStack = new ArrayList<T>();
450-
451446
protected @Auto char[] charBuffer;
452447

453448
protected int charBufferLen = 0;
@@ -2355,10 +2350,6 @@ public final void startTag(ElementName elementName,
23552350
if (selectedContentPointer != null) {
23562351
if (hasSelected) {
23572352
// Option with selected attr becomes active
2358-
// Clear previous selectedcontent content if we had a different active option
2359-
if (hadActiveOption && !seenSelectedOption) {
2360-
clearSelectedContentChildren();
2361-
}
23622353
seenSelectedOption = true;
23632354
shouldBeActive = true;
23642355
} else if (!seenSelectedOption && !hadActiveOption) {
@@ -2372,9 +2363,6 @@ public final void startTag(ElementName elementName,
23722363
if (shouldBeActive) {
23732364
activeOptionStackPos = currentPtr;
23742365
hadActiveOption = true;
2375-
// Initialize the clone stack with selectedcontent as the root parent
2376-
selectedContentCloneStack.clear();
2377-
selectedContentCloneStack.add(selectedContentPointer);
23782366
}
23792367
attributes = null; // CPP
23802368
break starttagloop;
@@ -5338,14 +5326,9 @@ private void pop() throws SAXException {
53385326
// This handles adoption agency restructuring correctly
53395327
if (currentPtr == activeOptionStackPos) {
53405328
if (selectedContentPointer != null) {
5341-
clearSelectedContentChildren();
5342-
deepCloneChildren(node.node, selectedContentPointer);
5329+
cloneOptionContentToSelectedContent(node.node, selectedContentPointer);
53435330
}
53445331
activeOptionStackPos = -1;
5345-
selectedContentCloneStack.clear();
5346-
} else if (activeOptionStackPos >= 0 && currentPtr > activeOptionStackPos && selectedContentCloneStack.size() > 1) {
5347-
// Pop from clone stack when popping an element inside active option
5348-
selectedContentCloneStack.remove(selectedContentCloneStack.size() - 1);
53495332
}
53505333
// Clear selectedcontent tracking if we're popping the select element
53515334
// (not when popping selectedcontent itself - the DOM node is still valid)
@@ -5355,7 +5338,6 @@ private void pop() throws SAXException {
53555338
seenSelectedOption = false;
53565339
activeOptionStackPos = -1;
53575340
hadActiveOption = false;
5358-
selectedContentCloneStack.clear();
53595341
}
53605342
currentPtr--;
53615343
elementPopped(node.ns, node.popName, node.node);
@@ -5371,14 +5353,9 @@ private void popForeign(int origPos, int eltPos) throws SAXException {
53715353
// When active option closes, deep-clone its content to selectedcontent
53725354
if (currentPtr == activeOptionStackPos) {
53735355
if (selectedContentPointer != null) {
5374-
clearSelectedContentChildren();
5375-
deepCloneChildren(node.node, selectedContentPointer);
5356+
cloneOptionContentToSelectedContent(node.node, selectedContentPointer);
53765357
}
53775358
activeOptionStackPos = -1;
5378-
selectedContentCloneStack.clear();
5379-
} else if (activeOptionStackPos >= 0 && currentPtr > activeOptionStackPos && selectedContentCloneStack.size() > 1) {
5380-
// Pop from clone stack when popping an element inside active option
5381-
selectedContentCloneStack.remove(selectedContentCloneStack.size() - 1);
53825359
}
53835360
// Clear selectedcontent tracking if we're popping the select element
53845361
if (node.getGroup() == SELECT) {
@@ -5387,7 +5364,6 @@ private void popForeign(int origPos, int eltPos) throws SAXException {
53875364
seenSelectedOption = false;
53885365
activeOptionStackPos = -1;
53895366
hadActiveOption = false;
5390-
selectedContentCloneStack.clear();
53915367
}
53925368
currentPtr--;
53935369
elementPopped(node.ns, node.popName, node.node);
@@ -5400,14 +5376,9 @@ private void silentPop() throws SAXException {
54005376
// When active option closes, deep-clone its content to selectedcontent
54015377
if (currentPtr == activeOptionStackPos) {
54025378
if (selectedContentPointer != null) {
5403-
clearSelectedContentChildren();
5404-
deepCloneChildren(node.node, selectedContentPointer);
5379+
cloneOptionContentToSelectedContent(node.node, selectedContentPointer);
54055380
}
54065381
activeOptionStackPos = -1;
5407-
selectedContentCloneStack.clear();
5408-
} else if (activeOptionStackPos >= 0 && currentPtr > activeOptionStackPos && selectedContentCloneStack.size() > 1) {
5409-
// Pop from clone stack when popping an element inside active option
5410-
selectedContentCloneStack.remove(selectedContentCloneStack.size() - 1);
54115382
}
54125383
// Clear selectedcontent tracking if we're popping the select element
54135384
if (node.getGroup() == SELECT) {
@@ -5416,7 +5387,6 @@ private void silentPop() throws SAXException {
54165387
seenSelectedOption = false;
54175388
activeOptionStackPos = -1;
54185389
hadActiveOption = false;
5419-
selectedContentCloneStack.clear();
54205390
}
54215391
currentPtr--;
54225392
node.release(this);
@@ -5428,14 +5398,9 @@ private void popOnEof() throws SAXException {
54285398
// When active option closes, deep-clone its content to selectedcontent
54295399
if (currentPtr == activeOptionStackPos) {
54305400
if (selectedContentPointer != null) {
5431-
clearSelectedContentChildren();
5432-
deepCloneChildren(node.node, selectedContentPointer);
5401+
cloneOptionContentToSelectedContent(node.node, selectedContentPointer);
54335402
}
54345403
activeOptionStackPos = -1;
5435-
selectedContentCloneStack.clear();
5436-
} else if (activeOptionStackPos >= 0 && currentPtr > activeOptionStackPos && selectedContentCloneStack.size() > 1) {
5437-
// Pop from clone stack when popping an element inside active option
5438-
selectedContentCloneStack.remove(selectedContentCloneStack.size() - 1);
54395404
}
54405405
// Clear selectedcontent tracking if we're popping the select element
54415406
if (node.getGroup() == SELECT) {
@@ -5444,7 +5409,6 @@ private void popOnEof() throws SAXException {
54445409
seenSelectedOption = false;
54455410
activeOptionStackPos = -1;
54465411
hadActiveOption = false;
5447-
selectedContentCloneStack.clear();
54485412
}
54495413
currentPtr--;
54505414
markMalformedIfScript(node.node);
@@ -5635,8 +5599,6 @@ private void appendToCurrentNodeAndPushFormattingElementMayFoster(
56355599
// ]NOCPP]
56365600
// This method can't be called for custom elements
56375601
HtmlAttributes clone = attributes.cloneAttributes();
5638-
// Clone to selectedcontent if inside active option (must be before createElement due to C++ attribute ownership)
5639-
cloneElementToSelectedContent("http://www.w3.org/1999/xhtml", elementName.getName(), attributes);
56405602
// Attributes must not be read after calling createElement, because
56415603
// createElement may delete attributes in C++.
56425604
T elt;
@@ -5686,8 +5648,6 @@ private void appendToCurrentNodeAndPushElement(ElementName elementName,
56865648
} else {
56875649
appendElement(elt, currentNode);
56885650
}
5689-
// Clone to selectedcontent if inside active option
5690-
cloneElementToSelectedContent("http://www.w3.org/1999/xhtml", elementName.getName(), attributes);
56915651
StackNode<T> node = createStackNode(elementName, elt
56925652
// [NOCPP[
56935653
, errorHandler == null ? null : new TaintableLocatorImpl(tokenizer)
@@ -5720,8 +5680,6 @@ private void appendToCurrentNodeAndPushElementMayFoster(ElementName elementName,
57205680
);
57215681
appendElement(elt, currentNode);
57225682
}
5723-
// Clone to selectedcontent if inside active option
5724-
cloneElementToSelectedContent("http://www.w3.org/1999/xhtml", popName, attributes);
57255683
StackNode<T> node = createStackNode(elementName, elt, popName
57265684
// [NOCPP[
57275685
, errorHandler == null ? null : new TaintableLocatorImpl(tokenizer)
@@ -5745,8 +5703,6 @@ private void appendToCurrentNodeAndPushElementMayFosterMathML(
57455703
&& annotationXmlEncodingPermitsHtml(attributes)) {
57465704
markAsHtmlIntegrationPoint = true;
57475705
}
5748-
// Clone to selectedcontent if inside active option (must be before createElement due to C++ attribute ownership)
5749-
cloneElementToSelectedContent("http://www.w3.org/1998/Math/MathML", popName, attributes);
57505706
// Attributes must not be read after calling createElement(), since
57515707
// createElement may delete the object in C++.
57525708
T elt;
@@ -5812,8 +5768,6 @@ private void appendToCurrentNodeAndPushElementMayFosterSVG(
58125768
popName = checkPopName(popName);
58135769
}
58145770
// ]NOCPP]
5815-
// Clone to selectedcontent if inside active option
5816-
cloneElementToSelectedContent("http://www.w3.org/2000/svg", popName, attributes);
58175771
T elt;
58185772
StackNode<T> current = stack[currentPtr];
58195773
if (current.isFosterParenting()) {
@@ -5843,8 +5797,6 @@ private void appendToCurrentNodeAndPushElementMayFoster(ElementName elementName,
58435797
checkAttributes(attributes, "http://www.w3.org/1999/xhtml");
58445798
// ]NOCPP]
58455799
// Can't be called for custom elements
5846-
// Clone to selectedcontent if inside active option
5847-
cloneElementToSelectedContent("http://www.w3.org/1999/xhtml", elementName.getName(), attributes);
58485800
T elt;
58495801
T formOwner = form == null || fragment || isTemplateContents() ? null : form;
58505802
StackNode<T> current = stack[currentPtr];
@@ -6033,55 +5985,6 @@ private void appendVoidFormToCurrent(HtmlAttributes attributes) throws SAXExcept
60335985
elementPopped("http://www.w3.org/1999/xhtml", "form", elt);
60345986
}
60355987

6036-
/**
6037-
* Clones an element to the selectedcontent hierarchy when inside an active option.
6038-
* This is used for the customizable select feature.
6039-
*
6040-
* @param ns The namespace URI
6041-
* @param name The element name
6042-
* @param attributes The attributes (will be cloned)
6043-
*/
6044-
private void cloneElementToSelectedContent(@NsUri String ns, @Local String name,
6045-
HtmlAttributes attributes) throws SAXException {
6046-
if (activeOptionStackPos < 0 || selectedContentCloneStack.isEmpty()) {
6047-
return;
6048-
}
6049-
// Clone the attributes
6050-
HtmlAttributes clonedAttrs = attributes.cloneAttributes();
6051-
// Get the current parent in the selectedcontent hierarchy
6052-
T selectedContentParent = selectedContentCloneStack.get(selectedContentCloneStack.size() - 1);
6053-
// Create a clone element
6054-
T clone = createElement(ns, name, clonedAttrs, selectedContentParent
6055-
// CPPONLY: , htmlCreator(null)
6056-
);
6057-
// Append the clone to the selectedcontent parent
6058-
appendElement(clone, selectedContentParent);
6059-
// Push the clone to the stack so nested content goes into it
6060-
selectedContentCloneStack.add(clone);
6061-
}
6062-
6063-
/**
6064-
* Clones a void element to the selectedcontent hierarchy when inside an active option.
6065-
* Void elements don't need to be pushed to the stack since they have no children.
6066-
*/
6067-
private void cloneVoidElementToSelectedContent(@NsUri String ns, @Local String name,
6068-
HtmlAttributes attributes) throws SAXException {
6069-
if (activeOptionStackPos < 0 || selectedContentCloneStack.isEmpty()) {
6070-
return;
6071-
}
6072-
// Clone the attributes
6073-
HtmlAttributes clonedAttrs = attributes.cloneAttributes();
6074-
// Get the current parent in the selectedcontent hierarchy
6075-
T selectedContentParent = selectedContentCloneStack.get(selectedContentCloneStack.size() - 1);
6076-
// Create a clone element
6077-
T clone = createElement(ns, name, clonedAttrs, selectedContentParent
6078-
// CPPONLY: , htmlCreator(null)
6079-
);
6080-
// Append the clone to the selectedcontent parent
6081-
appendElement(clone, selectedContentParent);
6082-
// Void elements don't need to be pushed to the stack
6083-
}
6084-
60855988
// [NOCPP[
60865989

60875990
private final void accumulateCharactersForced(@Const @NoLength char[] buf,
@@ -6117,10 +6020,6 @@ private final void accumulateCharactersForced(@Const @NoLength char[] buf,
61176020
protected void accumulateCharacters(@Const @NoLength char[] buf, int start,
61186021
int length) throws SAXException {
61196022
appendCharacters(stack[currentPtr].node, buf, start, length);
6120-
// Also clone to selectedcontent if in active option
6121-
if (activeOptionStackPos >= 0 && !selectedContentCloneStack.isEmpty()) {
6122-
appendCharacters(selectedContentCloneStack.get(selectedContentCloneStack.size() - 1), buf, start, length);
6123-
}
61246023
}
61256024

61266025
// ------------------------------- //
@@ -6149,12 +6048,14 @@ protected abstract T createHtmlElementSetAsRoot(HtmlAttributes attributes)
61496048
protected abstract void detachFromParent(T element) throws SAXException;
61506049

61516050
/**
6152-
* Deep clones the children of the source element to the destination element.
6153-
* Used for cloning option content to selectedcontent.
6154-
* Default implementation does nothing. Subclasses should override.
6051+
* Called when the active option is popped from the stack, to clone
6052+
* the option's children into selectedcontent. Subclasses that support
6053+
* DOM operations should override this to clear selectedcontent and
6054+
* deep-clone the option's children into it.
61556055
*/
6156-
protected void deepCloneChildren(T source, T destination) throws SAXException {
6157-
// Default implementation does nothing
6056+
protected void cloneOptionContentToSelectedContent(T option, T selectedContent)
6057+
throws SAXException {
6058+
// Default: no-op (streaming SAX mode ignores cloning)
61586059
}
61596060

61606061
protected abstract boolean hasChildren(T element) throws SAXException;
@@ -6200,16 +6101,6 @@ protected abstract void appendCommentToDocument(@NoLength char[] buf,
62006101
protected abstract void addAttributesToElement(T element,
62016102
HtmlAttributes attributes) throws SAXException;
62026103

6203-
/**
6204-
* Clears all children from the selectedcontent element.
6205-
* Used when an option with 'selected' attribute is seen after
6206-
* content has already been cloned from a previous option.
6207-
*/
6208-
protected void clearSelectedContentChildren() throws SAXException {
6209-
// Default implementation does nothing. Subclasses that support
6210-
// selectedcontent cloning can override this.
6211-
}
6212-
62136104
protected void markMalformedIfScript(T elt) throws SAXException {
62146105

62156106
}
@@ -6415,10 +6306,6 @@ && charBufferContainsNonWhitespace()) {
64156306
// reconstructing gave us a new current node
64166307
appendCharacters(currentNode(), charBuffer, 0,
64176308
charBufferLen);
6418-
// Also clone to selectedcontent if in active option
6419-
if (activeOptionStackPos >= 0 && !selectedContentCloneStack.isEmpty()) {
6420-
appendCharacters(selectedContentCloneStack.get(selectedContentCloneStack.size() - 1), charBuffer, 0, charBufferLen);
6421-
}
64226309
charBufferLen = 0;
64236310
return;
64246311
}
@@ -6428,29 +6315,17 @@ && charBufferContainsNonWhitespace()) {
64286315

64296316
if (templatePos >= tablePos) {
64306317
appendCharacters(stack[templatePos].node, charBuffer, 0, charBufferLen);
6431-
// Also clone to selectedcontent if in active option
6432-
if (activeOptionStackPos >= 0 && !selectedContentCloneStack.isEmpty()) {
6433-
appendCharacters(selectedContentCloneStack.get(selectedContentCloneStack.size() - 1), charBuffer, 0, charBufferLen);
6434-
}
64356318
charBufferLen = 0;
64366319
return;
64376320
}
64386321

64396322
StackNode<T> tableElt = stack[tablePos];
64406323
insertFosterParentedCharacters(charBuffer, 0, charBufferLen,
64416324
tableElt.node, stack[tablePos - 1].node);
6442-
// Also clone to selectedcontent if in active option
6443-
if (activeOptionStackPos >= 0 && !selectedContentCloneStack.isEmpty()) {
6444-
appendCharacters(selectedContentCloneStack.get(selectedContentCloneStack.size() - 1), charBuffer, 0, charBufferLen);
6445-
}
64466325
charBufferLen = 0;
64476326
return;
64486327
}
64496328
appendCharacters(currentNode(), charBuffer, 0, charBufferLen);
6450-
// Also clone to selectedcontent if in active option
6451-
if (activeOptionStackPos >= 0 && !selectedContentCloneStack.isEmpty()) {
6452-
appendCharacters(selectedContentCloneStack.get(selectedContentCloneStack.size() - 1), charBuffer, 0, charBufferLen);
6453-
}
64546329
charBufferLen = 0;
64556330
}
64566331
}

src/nu/validator/htmlparser/sax/SAXTreeBuilder.java

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -200,14 +200,13 @@ private Node previousSibling(Node table) {
200200
}
201201

202202
@Override
203-
protected void clearSelectedContentChildren() throws SAXException {
204-
if (selectedContentPointer != null) {
205-
((ParentNode) selectedContentPointer).clearChildren();
206-
}
203+
protected void cloneOptionContentToSelectedContent(Element option, Element selectedContent)
204+
throws SAXException {
205+
((ParentNode) selectedContent).clearChildren();
206+
deepCloneChildren(option, selectedContent);
207207
}
208208

209-
@Override
210-
protected void deepCloneChildren(Element source, Element destination) throws SAXException {
209+
private void deepCloneChildren(Element source, Element destination) throws SAXException {
211210
Node child = source.getFirstChild();
212211
while (child != null) {
213212
deepCloneNode(child, destination);

0 commit comments

Comments
 (0)