PureXML/Sources/Model/TreeNodeEditing.swift
Mihaela Mihaljevic 380160cc2b refactor(validation): decompose XSD instance validation into composable rules
The two monolithic XSD content/identity rules only satisfied the OpenAPIKit
validation idiom at the wrong granularity: no per-constraint removability,
no isolable failing/near-miss tests. Split them into 32 named rules, each
finding tagged at its origin and cached per XSDContext so the walk still
runs once per document (the #101 decision against duplicating XSD's
recursive type resolution still holds). Wired the streaming path's ID/IDREF
and identity checks through the same framework instead of calling the
validators imperatively, and added the unwrapAndLookup combinator the rule
already named as required.

Also includes the mandated repo-wide swiftformat/swiftlint pass.
2026-07-12 14:12:17 +02:00

106 lines
3.8 KiB
Swift

public extension PureXML.Model.TreeNode {
// MARK: Attribute helpers
/// The value of the attribute named `name` (by `prefix:local` rendering or by
/// local name), or nil when the element has no such attribute.
func attributeValue(_ name: String) -> String? {
attributes.first { $0.name.description == name || $0.name.localName == name }?.value
}
/// Sets the attribute named `name` to `value`, updating it in place when it is
/// already present (matched by `prefix:local` rendering) or appending it
/// otherwise. A no-op on a non-element node.
func setAttribute(_ name: String, _ value: String) {
guard kind == .element else { return }
if let index = attributes.firstIndex(where: { $0.name.description == name }) {
attributes[index].value = value
} else {
attributes.append(PureXML.Model.Attribute(name, value))
}
}
/// Removes the attribute named `name` (by `prefix:local` rendering or local
/// name), returning whether one was removed.
@discardableResult
func removeAttribute(_ name: String) -> Bool {
guard let index = attributes.firstIndex(where: { $0.name.description == name || $0.name.localName == name }) else {
return false
}
attributes.remove(at: index)
return true
}
// MARK: Copying
/// Returns a copy of this node alone, with no children and no parent. The
/// element's name, attributes, and value are carried over. Complements the
/// deep ``copy()``.
func shallowCopy() -> PureXML.Model.TreeNode {
PureXML.Model.TreeNode(kind: kind, name: name, attributes: attributes, value: value)
}
// MARK: Document order
/// Whether this node strictly precedes `other` in document order. Two nodes are
/// comparable only when they share a root; an unrelated node returns false. An
/// ancestor precedes its descendants.
func precedes(_ other: PureXML.Model.TreeNode) -> Bool {
if self === other {
return false
}
let mine = Self.pathFromRoot(self)
let theirs = Self.pathFromRoot(other)
guard mine.first === theirs.first else { return false }
var depth = 0
while depth < mine.count, depth < theirs.count, mine[depth] === theirs[depth] {
depth += 1
}
if depth == mine.count {
return true
}
if depth == theirs.count {
return false
}
let parent = mine[depth - 1]
let mineIndex = parent.children.firstIndex { $0 === mine[depth] } ?? 0
let theirsIndex = parent.children.firstIndex { $0 === theirs[depth] } ?? 0
return mineIndex < theirsIndex
}
private static func pathFromRoot(_ node: PureXML.Model.TreeNode) -> [PureXML.Model.TreeNode] {
var path = [node]
var current = node
while let parent = current.parent {
path.append(parent)
current = parent
}
return path.reversed()
}
// MARK: Normalization
/// Coalesces adjacent text children into one, drops empty text children, and
/// normalizes each element child recursively (the DOM `normalize` behavior).
/// CDATA sections are left intact and never merged with text.
func normalize() {
var result: [PureXML.Model.TreeNode] = []
for child in children {
if child.kind == .text {
if child.value.isEmpty {
child.parent = nil
continue
}
if let last = result.last, last.kind == .text {
last.value += child.value
child.parent = nil
continue
}
} else {
child.normalize()
}
result.append(child)
}
children = result
}
}