Add the DOM/tree editing primitives from the audit that the editor use case needs: - Attribute helpers: attributeValue(_:), setAttribute(_:_:) (update in place or append), removeAttribute(_:). - shallowCopy() copies a node alone, complementing the deep copy(). - precedes(_:) compares two nodes in document order (ancestor before descendant, earlier sibling before later; unrelated nodes compare false). - normalize() coalesces adjacent text children, drops empty text, and recurses into elements; CDATA is left intact. First-class namespace/DOCTYPE/entity-reference nodes (an invasive TreeNodeKind change) and cross-document adoption/ownerDocument (no document-identity model) are left for a follow-up. Closes #90
100 lines
3.8 KiB
Swift
100 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
|
|
}
|
|
}
|