Skip to content

Commit 638e4c0

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 bb54584 commit 638e4c0

File tree

2 files changed

+16
-142
lines changed

2 files changed

+16
-142
lines changed

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;
@@ -2362,10 +2357,6 @@ public final void startTag(ElementName elementName,
23622357
if (selectedContentPointer != null) {
23632358
if (hasSelected) {
23642359
// Option with selected attr becomes active
2365-
// Clear previous selectedcontent content if we had a different active option
2366-
if (hadActiveOption && !seenSelectedOption) {
2367-
clearSelectedContentChildren();
2368-
}
23692360
seenSelectedOption = true;
23702361
shouldBeActive = true;
23712362
} else if (!seenSelectedOption && !hadActiveOption) {
@@ -2379,9 +2370,6 @@ public final void startTag(ElementName elementName,
23792370
if (shouldBeActive) {
23802371
activeOptionStackPos = currentPtr;
23812372
hadActiveOption = true;
2382-
// Initialize the clone stack with selectedcontent as the root parent
2383-
selectedContentCloneStack.clear();
2384-
selectedContentCloneStack.add(selectedContentPointer);
23852373
}
23862374
attributes = null; // CPP
23872375
break starttagloop;
@@ -5346,14 +5334,9 @@ private void pop() throws SAXException {
53465334
// This handles adoption agency restructuring correctly
53475335
if (currentPtr == activeOptionStackPos) {
53485336
if (selectedContentPointer != null) {
5349-
clearSelectedContentChildren();
5350-
deepCloneChildren(node.node, selectedContentPointer);
5337+
cloneOptionContentToSelectedContent(node.node, selectedContentPointer);
53515338
}
53525339
activeOptionStackPos = -1;
5353-
selectedContentCloneStack.clear();
5354-
} else if (activeOptionStackPos >= 0 && currentPtr > activeOptionStackPos && selectedContentCloneStack.size() > 1) {
5355-
// Pop from clone stack when popping an element inside active option
5356-
selectedContentCloneStack.remove(selectedContentCloneStack.size() - 1);
53575340
}
53585341
// Clear selectedcontent tracking if we're popping the select element
53595342
// (not when popping selectedcontent itself - the DOM node is still valid)
@@ -5363,7 +5346,6 @@ private void pop() throws SAXException {
53635346
seenSelectedOption = false;
53645347
activeOptionStackPos = -1;
53655348
hadActiveOption = false;
5366-
selectedContentCloneStack.clear();
53675349
}
53685350
currentPtr--;
53695351
elementPopped(node.ns, node.popName, node.node);
@@ -5379,14 +5361,9 @@ private void popForeign(int origPos, int eltPos) throws SAXException {
53795361
// When active option closes, deep-clone its content to selectedcontent
53805362
if (currentPtr == activeOptionStackPos) {
53815363
if (selectedContentPointer != null) {
5382-
clearSelectedContentChildren();
5383-
deepCloneChildren(node.node, selectedContentPointer);
5364+
cloneOptionContentToSelectedContent(node.node, selectedContentPointer);
53845365
}
53855366
activeOptionStackPos = -1;
5386-
selectedContentCloneStack.clear();
5387-
} else if (activeOptionStackPos >= 0 && currentPtr > activeOptionStackPos && selectedContentCloneStack.size() > 1) {
5388-
// Pop from clone stack when popping an element inside active option
5389-
selectedContentCloneStack.remove(selectedContentCloneStack.size() - 1);
53905367
}
53915368
// Clear selectedcontent tracking if we're popping the select element
53925369
if (node.getGroup() == SELECT) {
@@ -5395,7 +5372,6 @@ private void popForeign(int origPos, int eltPos) throws SAXException {
53955372
seenSelectedOption = false;
53965373
activeOptionStackPos = -1;
53975374
hadActiveOption = false;
5398-
selectedContentCloneStack.clear();
53995375
}
54005376
currentPtr--;
54015377
elementPopped(node.ns, node.popName, node.node);
@@ -5408,14 +5384,9 @@ private void silentPop() throws SAXException {
54085384
// When active option closes, deep-clone its content to selectedcontent
54095385
if (currentPtr == activeOptionStackPos) {
54105386
if (selectedContentPointer != null) {
5411-
clearSelectedContentChildren();
5412-
deepCloneChildren(node.node, selectedContentPointer);
5387+
cloneOptionContentToSelectedContent(node.node, selectedContentPointer);
54135388
}
54145389
activeOptionStackPos = -1;
5415-
selectedContentCloneStack.clear();
5416-
} else if (activeOptionStackPos >= 0 && currentPtr > activeOptionStackPos && selectedContentCloneStack.size() > 1) {
5417-
// Pop from clone stack when popping an element inside active option
5418-
selectedContentCloneStack.remove(selectedContentCloneStack.size() - 1);
54195390
}
54205391
// Clear selectedcontent tracking if we're popping the select element
54215392
if (node.getGroup() == SELECT) {
@@ -5424,7 +5395,6 @@ private void silentPop() throws SAXException {
54245395
seenSelectedOption = false;
54255396
activeOptionStackPos = -1;
54265397
hadActiveOption = false;
5427-
selectedContentCloneStack.clear();
54285398
}
54295399
currentPtr--;
54305400
node.release(this);
@@ -5436,14 +5406,9 @@ private void popOnEof() throws SAXException {
54365406
// When active option closes, deep-clone its content to selectedcontent
54375407
if (currentPtr == activeOptionStackPos) {
54385408
if (selectedContentPointer != null) {
5439-
clearSelectedContentChildren();
5440-
deepCloneChildren(node.node, selectedContentPointer);
5409+
cloneOptionContentToSelectedContent(node.node, selectedContentPointer);
54415410
}
54425411
activeOptionStackPos = -1;
5443-
selectedContentCloneStack.clear();
5444-
} else if (activeOptionStackPos >= 0 && currentPtr > activeOptionStackPos && selectedContentCloneStack.size() > 1) {
5445-
// Pop from clone stack when popping an element inside active option
5446-
selectedContentCloneStack.remove(selectedContentCloneStack.size() - 1);
54475412
}
54485413
// Clear selectedcontent tracking if we're popping the select element
54495414
if (node.getGroup() == SELECT) {
@@ -5452,7 +5417,6 @@ private void popOnEof() throws SAXException {
54525417
seenSelectedOption = false;
54535418
activeOptionStackPos = -1;
54545419
hadActiveOption = false;
5455-
selectedContentCloneStack.clear();
54565420
}
54575421
currentPtr--;
54585422
markMalformedIfScript(node.node);
@@ -5643,8 +5607,6 @@ private void appendToCurrentNodeAndPushFormattingElementMayFoster(
56435607
// ]NOCPP]
56445608
// This method can't be called for custom elements
56455609
HtmlAttributes clone = attributes.cloneAttributes();
5646-
// Clone to selectedcontent if inside active option (must be before createElement due to C++ attribute ownership)
5647-
cloneElementToSelectedContent("http://www.w3.org/1999/xhtml", elementName.getName(), attributes);
56485610
// Attributes must not be read after calling createElement, because
56495611
// createElement may delete attributes in C++.
56505612
T elt;
@@ -5694,8 +5656,6 @@ private void appendToCurrentNodeAndPushElement(ElementName elementName,
56945656
} else {
56955657
appendElement(elt, currentNode);
56965658
}
5697-
// Clone to selectedcontent if inside active option
5698-
cloneElementToSelectedContent("http://www.w3.org/1999/xhtml", elementName.getName(), attributes);
56995659
StackNode<T> node = createStackNode(elementName, elt
57005660
// [NOCPP[
57015661
, errorHandler == null ? null : new TaintableLocatorImpl(tokenizer)
@@ -5728,8 +5688,6 @@ private void appendToCurrentNodeAndPushElementMayFoster(ElementName elementName,
57285688
);
57295689
appendElement(elt, currentNode);
57305690
}
5731-
// Clone to selectedcontent if inside active option
5732-
cloneElementToSelectedContent("http://www.w3.org/1999/xhtml", popName, attributes);
57335691
StackNode<T> node = createStackNode(elementName, elt, popName
57345692
// [NOCPP[
57355693
, errorHandler == null ? null : new TaintableLocatorImpl(tokenizer)
@@ -5753,8 +5711,6 @@ private void appendToCurrentNodeAndPushElementMayFosterMathML(
57535711
&& annotationXmlEncodingPermitsHtml(attributes)) {
57545712
markAsHtmlIntegrationPoint = true;
57555713
}
5756-
// Clone to selectedcontent if inside active option (must be before createElement due to C++ attribute ownership)
5757-
cloneElementToSelectedContent("http://www.w3.org/1998/Math/MathML", popName, attributes);
57585714
// Attributes must not be read after calling createElement(), since
57595715
// createElement may delete the object in C++.
57605716
T elt;
@@ -5820,8 +5776,6 @@ private void appendToCurrentNodeAndPushElementMayFosterSVG(
58205776
popName = checkPopName(popName);
58215777
}
58225778
// ]NOCPP]
5823-
// Clone to selectedcontent if inside active option
5824-
cloneElementToSelectedContent("http://www.w3.org/2000/svg", popName, attributes);
58255779
T elt;
58265780
StackNode<T> current = stack[currentPtr];
58275781
if (current.isFosterParenting()) {
@@ -5851,8 +5805,6 @@ private void appendToCurrentNodeAndPushElementMayFoster(ElementName elementName,
58515805
checkAttributes(attributes, "http://www.w3.org/1999/xhtml");
58525806
// ]NOCPP]
58535807
// Can't be called for custom elements
5854-
// Clone to selectedcontent if inside active option
5855-
cloneElementToSelectedContent("http://www.w3.org/1999/xhtml", elementName.getName(), attributes);
58565808
T elt;
58575809
T formOwner = form == null || fragment || isTemplateContents() ? null : form;
58585810
StackNode<T> current = stack[currentPtr];
@@ -6041,55 +5993,6 @@ private void appendVoidFormToCurrent(HtmlAttributes attributes) throws SAXExcept
60415993
elementPopped("http://www.w3.org/1999/xhtml", "form", elt);
60425994
}
60435995

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

60955998
private final void accumulateCharactersForced(@Const @NoLength char[] buf,
@@ -6125,10 +6028,6 @@ private final void accumulateCharactersForced(@Const @NoLength char[] buf,
61256028
protected void accumulateCharacters(@Const @NoLength char[] buf, int start,
61266029
int length) throws SAXException {
61276030
appendCharacters(stack[currentPtr].node, buf, start, length);
6128-
// Also clone to selectedcontent if in active option
6129-
if (activeOptionStackPos >= 0 && !selectedContentCloneStack.isEmpty()) {
6130-
appendCharacters(selectedContentCloneStack.get(selectedContentCloneStack.size() - 1), buf, start, length);
6131-
}
61326031
}
61336032

61346033
// ------------------------------- //
@@ -6157,12 +6056,14 @@ protected abstract T createHtmlElementSetAsRoot(HtmlAttributes attributes)
61576056
protected abstract void detachFromParent(T element) throws SAXException;
61586057

61596058
/**
6160-
* Deep clones the children of the source element to the destination element.
6161-
* Used for cloning option content to selectedcontent.
6162-
* Default implementation does nothing. Subclasses should override.
6059+
* Called when the active option is popped from the stack, to clone
6060+
* the option's children into selectedcontent. Subclasses that support
6061+
* DOM operations should override this to clear selectedcontent and
6062+
* deep-clone the option's children into it.
61636063
*/
6164-
protected void deepCloneChildren(T source, T destination) throws SAXException {
6165-
// Default implementation does nothing
6064+
protected void cloneOptionContentToSelectedContent(T option, T selectedContent)
6065+
throws SAXException {
6066+
// Default: no-op (streaming SAX mode ignores cloning)
61666067
}
61676068

61686069
protected abstract boolean hasChildren(T element) throws SAXException;
@@ -6208,16 +6109,6 @@ protected abstract void appendCommentToDocument(@NoLength char[] buf,
62086109
protected abstract void addAttributesToElement(T element,
62096110
HtmlAttributes attributes) throws SAXException;
62106111

6211-
/**
6212-
* Clears all children from the selectedcontent element.
6213-
* Used when an option with 'selected' attribute is seen after
6214-
* content has already been cloned from a previous option.
6215-
*/
6216-
protected void clearSelectedContentChildren() throws SAXException {
6217-
// Default implementation does nothing. Subclasses that support
6218-
// selectedcontent cloning can override this.
6219-
}
6220-
62216112
protected void markMalformedIfScript(T elt) throws SAXException {
62226113

62236114
}
@@ -6423,10 +6314,6 @@ && charBufferContainsNonWhitespace()) {
64236314
// reconstructing gave us a new current node
64246315
appendCharacters(currentNode(), charBuffer, 0,
64256316
charBufferLen);
6426-
// Also clone to selectedcontent if in active option
6427-
if (activeOptionStackPos >= 0 && !selectedContentCloneStack.isEmpty()) {
6428-
appendCharacters(selectedContentCloneStack.get(selectedContentCloneStack.size() - 1), charBuffer, 0, charBufferLen);
6429-
}
64306317
charBufferLen = 0;
64316318
return;
64326319
}
@@ -6436,29 +6323,17 @@ && charBufferContainsNonWhitespace()) {
64366323

64376324
if (templatePos >= tablePos) {
64386325
appendCharacters(stack[templatePos].node, charBuffer, 0, charBufferLen);
6439-
// Also clone to selectedcontent if in active option
6440-
if (activeOptionStackPos >= 0 && !selectedContentCloneStack.isEmpty()) {
6441-
appendCharacters(selectedContentCloneStack.get(selectedContentCloneStack.size() - 1), charBuffer, 0, charBufferLen);
6442-
}
64436326
charBufferLen = 0;
64446327
return;
64456328
}
64466329

64476330
StackNode<T> tableElt = stack[tablePos];
64486331
insertFosterParentedCharacters(charBuffer, 0, charBufferLen,
64496332
tableElt.node, stack[tablePos - 1].node);
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-
}
64546333
charBufferLen = 0;
64556334
return;
64566335
}
64576336
appendCharacters(currentNode(), charBuffer, 0, charBufferLen);
6458-
// Also clone to selectedcontent if in active option
6459-
if (activeOptionStackPos >= 0 && !selectedContentCloneStack.isEmpty()) {
6460-
appendCharacters(selectedContentCloneStack.get(selectedContentCloneStack.size() - 1), charBuffer, 0, charBufferLen);
6461-
}
64626337
charBufferLen = 0;
64636338
}
64646339
}

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)