Skip to content

Commit dda35a1

Browse files
committed
Add move and more tests
1 parent ce2c0e5 commit dda35a1

7 files changed

+83
-21
lines changed

Sources/Nodal/Document/Document+PendingNameRecords.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ internal extension Document {
4242
for key in keys { pendingNamespaceRecords[key] = nil }
4343
}
4444

45-
func pendingNameRecords(forDescendantsOf parent: Element) -> [(Element, PendingNameRecord)] {
45+
func pendingNameRecords(forDescendantsOf parent: Node) -> [(Element, PendingNameRecord)] {
4646
pendingNamespaceRecords.compactMap {
4747
$1.belongsToTree(parent) ? (element(for: .init($0)), $1) : nil
4848
}

Sources/Nodal/Element/Element.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import pugixml
66
/// - Note: This class provides functionality for working with XML elements, including accessing their attributes,
77
/// child nodes, and text content. It extends the `Node` class, inheriting its methods and properties.
88
public class Element: Node {
9-
override func declaredNamespacesDidChange() {
9+
internal override func declaredNamespacesDidChange() {
1010
let namespaces = declaredNamespaces
1111
for (element, record) in document.pendingNameRecords(forDescendantsOf: self) {
1212
if record.attemptResolution(for: element, with: namespaces) {
@@ -15,7 +15,7 @@ public class Element: Node {
1515
}
1616
}
1717

18-
override var hasNamespaceDeclarations: Bool {
18+
internal override var hasNamespaceDeclarations: Bool {
1919
node.attributes.contains(where: { String(cString: $0.name()).hasPrefix("xmlns") })
2020
}
2121
}

Sources/Nodal/Element/PendingNameRecord.swift

+9
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@ internal class PendingNameRecord {
1616
}
1717
}
1818

19+
func updateAncestors(with element: Element) {
20+
ancestors = []
21+
var node = element.node
22+
while !node.empty() {
23+
ancestors.insert(node)
24+
node = node.parent()
25+
}
26+
}
27+
1928
func belongsToTree(_ node: Node) -> Bool {
2029
ancestors.contains(node.node)
2130
}

Sources/Nodal/Node/Node+Hierarchy.swift

+26
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,32 @@ public extension Node {
9999
}
100100
}
101101

102+
public extension Node {
103+
/// Moves this node to a new parent node at the specified position within the parent's children.
104+
///
105+
/// - Parameters:
106+
/// - parent: The new parent node to which this node should be moved.
107+
/// - position: The position within the parent's children where this node should be inserted. Defaults to `.last`, adding the node as the last child of the parent.
108+
/// - Returns: A Boolean value indicating whether the move was successful.
109+
/// Returns `false` if the node cannot be moved. Examples of such cases include:
110+
/// - The new parent node belongs to a different document.
111+
/// - The node is being moved to within itself, which would create an invalid structure.
112+
@discardableResult
113+
func move(to parent: Node, at position: Position = .last) -> Bool {
114+
let records = document.pendingNameRecords(forDescendantsOf: self)
115+
var destination = parent.node
116+
117+
if destination.insertChild(self.node, at: position).empty() {
118+
return false
119+
}
120+
121+
for (element, record) in records {
122+
record.updateAncestors(with: element)
123+
}
124+
return true
125+
}
126+
}
127+
102128
public extension Node {
103129
/// Adds a new comment with the specified content to this node at the given position.
104130
///

Sources/Nodal/Node/Node.swift

+3-3
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ public class Node {
5757
}
5858
}
5959

60-
func declaredNamespacesDidChange() {}
61-
var hasNamespaceDeclarations: Bool { false }
60+
internal func declaredNamespacesDidChange() {}
61+
internal var hasNamespaceDeclarations: Bool { false }
6262

6363
/// The document that owns this node.
6464
///
@@ -151,7 +151,7 @@ extension Node: CustomDebugStringConvertible {
151151
case .text: "Text \"\(value)\""
152152
case .cdata: "CDATA \"\(value)\""
153153
case .comment: "Comment <!--\(value)-->"
154-
case .doctype: "<!DOCTYPE \(value)>"
154+
case .doctype: "DOCTYPE <!DOCTYPE \(value)>"
155155
case .processingInstruction: "PI <?\(name) \(value)?>"
156156
case .declaration: "Declaration <?\(name)...?>"
157157
case .document: "Document"

Sources/Nodal/Pugi.swift

+13
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,17 @@ internal extension pugi.xml_node {
3535
case .last: append_child(kind)
3636
}
3737
}
38+
39+
mutating func insertChild(_ child: pugi.xml_node, at childPosition: Node.Position) -> pugi.xml_node {
40+
guard childPosition.validate(for: self) else {
41+
fatalError("Peer node for Node.Position must be a valid child of the parent")
42+
}
43+
44+
return switch childPosition {
45+
case .first: prepend_move(child)
46+
case .before (let other): insert_move_before(child, other.node)
47+
case .after (let other): insert_move_after(child, other.node)
48+
case .last: append_move(child)
49+
}
50+
}
3851
}

Sources/Tests/Tests.swift

+29-15
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ struct Tests {
8787
}
8888

8989
@Test
90-
func testInvalidation() throws {
90+
func invalidation() throws {
9191
let doc = Document()
9292
let root = doc.makeDocumentElement(name: "root")
9393
let a = root.addElement("a")
@@ -135,20 +135,34 @@ struct Tests {
135135
}
136136

137137
@Test
138-
func lab() throws {
139-
let doc = try Document(string: """
140-
<root>
141-
foo
142-
<a>
143-
bar
144-
<b>baz<!--comment--><![CDATA[zoing]]>biz</b>
145-
doz
146-
</a>
147-
</root>
148-
""", options: [.default, .trimTextWhitespace])
138+
func move() throws {
139+
let doc = Document()
140+
let root = doc.makeDocumentElement(name: "root")
141+
let a = root.addElement("a")
142+
let b = root.addElement("b")
143+
let c = a.addComment("hello")
144+
145+
#expect(c.move(to: a) == true, "Successful move")
146+
#expect(Array(a.children) == [c], "Destination has target")
147+
#expect(a.move(to: b) == true, "Successful move with children")
148+
#expect(c.parent?.parent == b, "Grandparent is correct")
149+
150+
let doc2 = Document()
151+
let root2 = doc2.makeDocumentElement(name: "root2")
152+
#expect(c.move(to: root2) == false, "Move between documents")
153+
#expect(c.move(to: c) == false, "Move to itself")
154+
}
155+
156+
@Test
157+
func addAt() throws {
158+
let doc = Document()
159+
let root = doc.makeDocumentElement(name: "root")
160+
let a = root.addElement("a")
161+
let b = root.addElement("b", at: .first)
162+
163+
#expect(Array(root.children) == [b, a], "Order of children")
164+
let c = root.addCDATA("c", at: .after(b))
165+
#expect(Array(root.children) == [b, c, a], "Order of children")
149166

150-
for node in doc.descendants {
151-
print("Found \(node)!")
152-
}
153167
}
154168
}

0 commit comments

Comments
 (0)