PureXML/Sources/XPath/Selection.swift
Mihaela Mihaljevic 48c2c5c7db
fix(xpath): make string-value extraction iterative and comment-correct (#351)
Computing a node's string-value walked its subtree recursively, so it
overflowed the stack on a deeply-nested node (around 2000 deep on a Swift
Task stack): `XPath.Query("/a/a").strings(over:)` over a 4000-deep chain
crashed. `TreeNode.textContent` and the value-Node string-value behind
`Selection.stringValue` now walk an explicit stack, pushing children reversed
to concatenate in document order.

The value-Node path also wrongly folded descendant comment and
processing-instruction data into an element's string-value, disagreeing with
the tree-node path and XPath 1.0 (5.1/5.5: only text and CDATA contribute to
an element's string-value; a comment or PI node's own string-value, 5.6/5.7,
is still its data). It now matches. No test depended on the old behaviour:
1789 pass, including the XPath and XSLT value corpora.

Closes the string-value sites of #350; the transform walks (canonicalize,
DTD defaults, XSLT result tree) remain tracked there.
2026-06-26 20:44:49 +02:00

62 lines
2.5 KiB
Swift

public extension PureXML.XPath {
/// A node selected by an XPath query: either a tree node or an attribute
/// (attributes are not ``PureXML/Model/Node`` values, so they are surfaced
/// here alongside nodes).
enum Selection: Equatable, Sendable {
case node(PureXML.Model.Node)
case attribute(PureXML.Model.Attribute)
/// The selected element, when this selection is an element node.
public var element: PureXML.Model.Element? {
guard case let .node(node) = self, case let .element(element) = node else {
return nil
}
return element
}
/// The XPath string-value: an attribute's value, or the concatenation of
/// all descendant character data for a node.
public var stringValue: String {
switch self {
case let .attribute(attribute):
attribute.value
case let .node(node):
Self.stringValue(of: node)
}
}
private static func stringValue(of node: PureXML.Model.Node) -> String {
switch node {
// A comment or processing-instruction node's own string-value is its
// data (XPath 1.0 5.6, 5.7), but as a descendant of an element it
// contributes nothing (5.1, 5.5): only text and CDATA do.
case let .comment(value):
return value
case let .processingInstruction(_, data):
return data
case let .text(value), let .cdata(value):
return value
case .element, .document:
// Iterative pre-order walk so a deeply-nested node does not
// overflow the stack; children are pushed reversed to concatenate
// descendant text in document order.
var result = ""
var stack: [PureXML.Model.Node] = [node]
while let current = stack.popLast() {
switch current {
case let .text(value), let .cdata(value):
result += value
case let .element(element):
stack.append(contentsOf: element.children.reversed())
case let .document(children):
stack.append(contentsOf: children.reversed())
case .comment, .processingInstruction:
break
}
}
return result
}
}
}
}