Skip to content

Commit

Permalink
Add move and more tests
Browse files Browse the repository at this point in the history
  • Loading branch information
tomasf committed Jan 22, 2025
1 parent ce2c0e5 commit dda35a1
Show file tree
Hide file tree
Showing 7 changed files with 83 additions and 21 deletions.
2 changes: 1 addition & 1 deletion Sources/Nodal/Document/Document+PendingNameRecords.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ internal extension Document {
for key in keys { pendingNamespaceRecords[key] = nil }
}

func pendingNameRecords(forDescendantsOf parent: Element) -> [(Element, PendingNameRecord)] {
func pendingNameRecords(forDescendantsOf parent: Node) -> [(Element, PendingNameRecord)] {
pendingNamespaceRecords.compactMap {
$1.belongsToTree(parent) ? (element(for: .init($0)), $1) : nil
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/Nodal/Element/Element.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import pugixml
/// - Note: This class provides functionality for working with XML elements, including accessing their attributes,
/// child nodes, and text content. It extends the `Node` class, inheriting its methods and properties.
public class Element: Node {
override func declaredNamespacesDidChange() {
internal override func declaredNamespacesDidChange() {
let namespaces = declaredNamespaces
for (element, record) in document.pendingNameRecords(forDescendantsOf: self) {
if record.attemptResolution(for: element, with: namespaces) {
Expand All @@ -15,7 +15,7 @@ public class Element: Node {
}
}

override var hasNamespaceDeclarations: Bool {
internal override var hasNamespaceDeclarations: Bool {
node.attributes.contains(where: { String(cString: $0.name()).hasPrefix("xmlns") })
}
}
9 changes: 9 additions & 0 deletions Sources/Nodal/Element/PendingNameRecord.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ internal class PendingNameRecord {
}
}

func updateAncestors(with element: Element) {
ancestors = []
var node = element.node
while !node.empty() {
ancestors.insert(node)
node = node.parent()
}
}

func belongsToTree(_ node: Node) -> Bool {
ancestors.contains(node.node)
}
Expand Down
26 changes: 26 additions & 0 deletions Sources/Nodal/Node/Node+Hierarchy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,32 @@ public extension Node {
}
}

public extension Node {
/// Moves this node to a new parent node at the specified position within the parent's children.
///
/// - Parameters:
/// - parent: The new parent node to which this node should be moved.
/// - 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.
/// - Returns: A Boolean value indicating whether the move was successful.
/// Returns `false` if the node cannot be moved. Examples of such cases include:
/// - The new parent node belongs to a different document.
/// - The node is being moved to within itself, which would create an invalid structure.
@discardableResult
func move(to parent: Node, at position: Position = .last) -> Bool {
let records = document.pendingNameRecords(forDescendantsOf: self)
var destination = parent.node

if destination.insertChild(self.node, at: position).empty() {
return false
}

for (element, record) in records {
record.updateAncestors(with: element)
}
return true
}
}

public extension Node {
/// Adds a new comment with the specified content to this node at the given position.
///
Expand Down
6 changes: 3 additions & 3 deletions Sources/Nodal/Node/Node.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ public class Node {
}
}

func declaredNamespacesDidChange() {}
var hasNamespaceDeclarations: Bool { false }
internal func declaredNamespacesDidChange() {}
internal var hasNamespaceDeclarations: Bool { false }

/// The document that owns this node.
///
Expand Down Expand Up @@ -151,7 +151,7 @@ extension Node: CustomDebugStringConvertible {
case .text: "Text \"\(value)\""
case .cdata: "CDATA \"\(value)\""
case .comment: "Comment <!--\(value)-->"
case .doctype: "<!DOCTYPE \(value)>"
case .doctype: "DOCTYPE <!DOCTYPE \(value)>"
case .processingInstruction: "PI <?\(name) \(value)?>"
case .declaration: "Declaration <?\(name)...?>"
case .document: "Document"
Expand Down
13 changes: 13 additions & 0 deletions Sources/Nodal/Pugi.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,17 @@ internal extension pugi.xml_node {
case .last: append_child(kind)
}
}

mutating func insertChild(_ child: pugi.xml_node, at childPosition: Node.Position) -> pugi.xml_node {
guard childPosition.validate(for: self) else {
fatalError("Peer node for Node.Position must be a valid child of the parent")
}

return switch childPosition {
case .first: prepend_move(child)
case .before (let other): insert_move_before(child, other.node)
case .after (let other): insert_move_after(child, other.node)
case .last: append_move(child)
}
}
}
44 changes: 29 additions & 15 deletions Sources/Tests/Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ struct Tests {
}

@Test
func testInvalidation() throws {
func invalidation() throws {
let doc = Document()
let root = doc.makeDocumentElement(name: "root")
let a = root.addElement("a")
Expand Down Expand Up @@ -135,20 +135,34 @@ struct Tests {
}

@Test
func lab() throws {
let doc = try Document(string: """
<root>
foo
<a>
bar
<b>baz<!--comment--><![CDATA[zoing]]>biz</b>
doz
</a>
</root>
""", options: [.default, .trimTextWhitespace])
func move() throws {
let doc = Document()
let root = doc.makeDocumentElement(name: "root")
let a = root.addElement("a")
let b = root.addElement("b")
let c = a.addComment("hello")

#expect(c.move(to: a) == true, "Successful move")
#expect(Array(a.children) == [c], "Destination has target")
#expect(a.move(to: b) == true, "Successful move with children")
#expect(c.parent?.parent == b, "Grandparent is correct")

let doc2 = Document()
let root2 = doc2.makeDocumentElement(name: "root2")
#expect(c.move(to: root2) == false, "Move between documents")
#expect(c.move(to: c) == false, "Move to itself")
}

@Test
func addAt() throws {
let doc = Document()
let root = doc.makeDocumentElement(name: "root")
let a = root.addElement("a")
let b = root.addElement("b", at: .first)

#expect(Array(root.children) == [b, a], "Order of children")
let c = root.addCDATA("c", at: .after(b))
#expect(Array(root.children) == [b, c, a], "Order of children")

for node in doc.descendants {
print("Found \(node)!")
}
}
}

0 comments on commit dda35a1

Please sign in to comment.