Skip to content

Commit a93f371

Browse files
committed
Add XMLElementCodable
1 parent 6de41cb commit a93f371

12 files changed

+541
-389
lines changed

Sources/Nodal/Node/Node+Attributes.swift

+104-27
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,33 @@
11
import Foundation
22
import pugixml
33

4+
internal extension Node {
5+
func value(forAttribute name: String) -> String? {
6+
let attribute = node.attribute(name)
7+
return attribute.empty() ? nil : String(cString: attribute.value())
8+
}
9+
10+
func setValue(_ value: String?, forAttribute name: String) {
11+
var node = node
12+
var attr = node.attribute(name)
13+
if attr.empty() {
14+
if value != nil {
15+
attr = node.append_attribute(name)
16+
attr.set_value(value)
17+
}
18+
} else {
19+
if let value {
20+
attr.set_value(value)
21+
} else {
22+
node.remove_attribute(attr)
23+
}
24+
}
25+
if name.hasPrefix("xmlns") {
26+
document.declaredNamespacesDidChange(for: self)
27+
}
28+
}
29+
}
30+
431
public extension Node {
532
/// A Boolean value indicating whether this node type supports attributes.
633
///
@@ -49,42 +76,92 @@ public extension Node {
4976
}
5077
}
5178
}
79+
}
5280

53-
/// Accesses the value of a specific attribute by its qualified name.
81+
public extension Node {
82+
/// The attributes of this element in document order, represented as an array of expanded names and their corresponding values.
5483
///
55-
/// - Parameter name: The qualified name of the attribute to access.
56-
/// - Returns: The value of the attribute if it exists, or `nil` if the attribute is not present.
84+
/// - Returns: An array of tuples where each tuple contains an `ExpandedName` and the corresponding attribute value.
5785
///
58-
/// - Example:
59-
/// ```swift
60-
/// let element = ...
61-
/// element["id"] = "12345" // Sets the "id" attribute
62-
/// let idValue = element["id"] // Retrieves the value of the "id" attribute
63-
/// element["class"] = nil // Removes the "class" attribute
64-
/// ```
65-
subscript(attribute name: String) -> String? {
86+
/// - Note: Setting this property replaces all existing attributes with the new ones, preserving the specified order.
87+
var orderedAttributes: [(name: ExpandedName, value: String)] {
6688
get {
67-
let attribute = node.attribute(name)
68-
return attribute.empty() ? nil : String(cString: attribute.value())
89+
return node.attributes.map {(
90+
ExpandedName(effectiveQualifiedAttributeName: String(cString: $0.name()), in: self),
91+
String(cString: $0.value())
92+
)}
6993
}
7094
nonmutating set {
7195
var node = node
72-
var attr = node.attribute(name)
73-
if attr.empty() {
74-
if newValue != nil {
75-
attr = node.append_attribute(name)
76-
attr.set_value(newValue)
77-
}
78-
} else {
79-
if let newValue {
80-
attr.set_value(newValue)
81-
} else {
82-
node.remove_attribute(attr)
83-
}
96+
node.remove_attributes()
97+
for (name, value) in newValue {
98+
let qName = name.requestQualifiedAttributeName(for: self)
99+
var attr = node.append_attribute(qName)
100+
attr.set_value(value)
84101
}
85-
if name.hasPrefix("xmlns") {
86-
document.declaredNamespacesDidChange(for: self)
102+
}
103+
}
104+
105+
/// The attributes of this element as a dictionary, where the keys are `ExpandedName` objects and the values are their corresponding attribute values.
106+
///
107+
/// - Returns: A dictionary of attributes keyed by their expanded names.
108+
///
109+
/// - Note: Setting this property replaces all existing attributes with the new ones. The order of attributes is determined by the dictionary's order.
110+
var namespacedAttributes: [ExpandedName: String] {
111+
get { Dictionary(orderedAttributes) { $1 } }
112+
nonmutating set { orderedAttributes = newValue.map { ($0, $1) } }
113+
}
114+
115+
/// Accesses the value of an attribute by its name.
116+
///
117+
/// - Parameter name: The name of the attribute to access; either a `String` or an `ExpandedName`.
118+
/// - Returns: The value of the attribute if it exists, or `nil` if no such attribute is found.
119+
///
120+
/// - Note: When setting an attribute with an expanded name, its namespace is resolved based on the current scope. If `nil` is assigned, the attribute is removed.
121+
///
122+
/// - Example:
123+
/// ```swift
124+
/// let name = ExpandedName(namespaceName: "http://example.com", localName: "attribute")
125+
/// element[attribute: name] = "value" // Adds or updates the attribute
126+
/// let value = element[attribute: name] // Retrieves the value
127+
/// element[attribute: name] = nil // Removes the attribute
128+
/// ```
129+
subscript(attribute name: AttributeName) -> String? {
130+
get {
131+
if let qName = name.qualifiedName(in: self) {
132+
return value(forAttribute: qName)
133+
} else {
134+
return nil
87135
}
88136
}
137+
nonmutating set {
138+
let qName = name.requestQualifiedName(in: self)
139+
setValue(newValue, forAttribute: qName)
140+
}
141+
}
142+
143+
/// Accesses the value of an attribute by its local name and optional namespace URI.
144+
///
145+
/// - Parameters:
146+
/// - localName: The local name of the attribute to access.
147+
/// - namespaceURI: The namespace name of the attribute, or `nil` if the attribute is not namespaced.
148+
/// - Returns: The value of the attribute if it exists, or `nil` if no such attribute is found.
149+
///
150+
/// - Note: This subscript allows convenient access to attributes by specifying both the local name and namespace.
151+
///
152+
/// - Example:
153+
/// ```swift
154+
/// element[attribute: "id", namespaceName: nil] = "123" // Sets an attribute with no namespace
155+
/// element[attribute: "name", namespaceName: "http://example.com"] = "example" // Sets a namespaced attribute
156+
/// let value = element[attribute: "name", namespaceName: "http://example.com"] // Retrieves the value
157+
/// element[attribute: "name", namespaceName: "http://example.com"] = nil // Removes the attribute
158+
/// ```
159+
subscript(attribute localName: String, namespaceName namespaceURI: String?) -> String? {
160+
get {
161+
self[attribute: ExpandedName(namespaceName: namespaceURI, localName: localName)]
162+
}
163+
nonmutating set {
164+
self[attribute: ExpandedName(namespaceName: namespaceURI, localName: localName)] = newValue
165+
}
89166
}
90167
}

Sources/Nodal/Node/Node+Elements.swift

+11-48
Original file line numberDiff line numberDiff line change
@@ -19,33 +19,21 @@ public extension Node {
1919

2020
/// Retrieves the first child element with the specified name.
2121
///
22-
/// - Parameter name: The qualified name of the child element to retrieve.
22+
/// - Parameter name: The name of the child element to retrieve; either a `String` or an `ExpandedName`.
2323
/// - Returns: The first child element with the specified name, or `nil` if no such element exists.
24-
subscript(element name: String) -> Node? {
25-
node.children.first {
26-
$0.type() == pugi.node_element && String(cString: $0.name()) == name
24+
subscript(element name: any ElementName) -> Node? {
25+
node.children.lazy.first {
26+
$0.type() == pugi.node_element && name.matches(node: $0, in: document)
2727
}?.wrapped(in: document)
2828
}
2929

3030
/// Retrieves all child elements with the specified name.
3131
///
32-
/// - Parameter name: The qualified name of the child elements to retrieve.
32+
/// - Parameter name: The name of the child elements to retrieve; either a `String` or an `ExpandedName`.
3333
/// - Returns: An array of child elements with the specified name.
34-
subscript(elements name: String) -> [Node] {
34+
subscript(elements name: any ElementName) -> [Node] {
3535
node.children.lazy.filter {
36-
$0.type() == pugi.node_element && String(cString: $0.name()) == name
37-
}.map { $0.wrapped(in: document) }
38-
}
39-
40-
/// Retrieves all child elements matching the specified expanded name.
41-
///
42-
/// - Parameter targetName: The expanded name (including local name and optional namespace) of the elements to retrieve.
43-
/// - Returns: An array of child elements matching the expanded name.
44-
subscript(elements targetName: ExpandedName) -> [Node] {
45-
return node.children.lazy.filter {
46-
$0.type() == pugi.node_element
47-
&& String(cString: $0.name()).hasSuffix(targetName.localName)
48-
&& self.document.expandedName(for: $0) == targetName
36+
$0.type() == pugi.node_element && name.matches(node: $0, in: document)
4937
}.map { $0.wrapped(in: document) }
5038
}
5139

@@ -59,18 +47,6 @@ public extension Node {
5947
self[elements: ExpandedName(namespaceName: namespaceURI, localName: localName)]
6048
}
6149

62-
/// Retrieves the first child element matching the specified expanded name.
63-
///
64-
/// - Parameter targetName: The expanded name (including local name and optional namespace) of the element to retrieve.
65-
/// - Returns: The first child element matching the expanded name, or `nil` if no such element exists.
66-
subscript(element targetName: ExpandedName) -> Node? {
67-
node.children.lazy.first {
68-
$0.type() == pugi.node_element
69-
&& String(cString: $0.name()).hasSuffix(targetName.localName)
70-
&& document.expandedName(for: $0) == targetName
71-
}?.wrapped(in: document)
72-
}
73-
7450
/// Retrieves the first child element with the specified local name and namespace URI.
7551
///
7652
/// - Parameters:
@@ -83,33 +59,20 @@ public extension Node {
8359
}
8460

8561
public extension Node {
86-
/// Adds a new child element with the specified qualified name to this element at the given position.
62+
/// Adds a new child element with the specified name to this element at the given position.
8763
///
8864
/// - Parameters:
89-
/// - name: The qualified name of the new element.
65+
/// - name: The name of the new element; either a `String` or an `ExpandedName`.
9066
/// - position: The position where the new child element should be inserted. Defaults to `.last`, adding the element as the last child of this element.
9167
/// - Returns: The newly created child element.
9268
@discardableResult
93-
func addElement(_ name: String, at position: Position = .last) -> Node {
69+
func addElement(_ name: any ElementName, at position: Position = .last) -> Node {
9470
precondition(canContainChildren(ofKind: .element), "This kind of node can't contain elements")
9571
let element = document.node(for: node.addChild(kind: pugi.node_element, at: position))
96-
element.name = name
72+
element.name = name.requestQualifiedName(for: element)
9773
return element
9874
}
9975

100-
/// Adds a new child element with the specified expanded name to this element at the given position.
101-
///
102-
/// - Parameters:
103-
/// - name: The expanded name of the new element, including the local name and an optional namespace.
104-
/// - position: The position where the new child element should be inserted. Defaults to `.last`, adding the element as the last child of this element.
105-
/// - Returns: The newly created child element.
106-
@discardableResult
107-
func addElement(_ name: ExpandedName, at position: Position = .last) -> Node {
108-
let child = addElement("", at: position)
109-
child.expandedName = name
110-
return child
111-
}
112-
11376
/// Adds a new child element with the specified local name and optional namespace URI to this element at the given position.
11477
///
11578
/// - Parameters:

Sources/Nodal/Node/Node+Names.swift

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import Foundation
2+
import pugixml
3+
4+
public protocol ElementName: Sendable {
5+
func requestQualifiedName(for node: Node) -> String
6+
func matches(node: pugi.xml_node, in document: Document) -> Bool
7+
}
8+
9+
extension String: ElementName {
10+
public func requestQualifiedName(for node: Node) -> String {
11+
self
12+
}
13+
public func matches(node: pugi.xml_node, in document: Document) -> Bool {
14+
String(cString: node.name()) == self
15+
}
16+
}
17+
18+
extension ExpandedName: ElementName {
19+
public func requestQualifiedName(for node: Node) -> String {
20+
requestQualifiedElementName(for: node)
21+
}
22+
23+
public func matches(node: pugi.xml_node, in document: Document) -> Bool {
24+
String(cString: node.name()).hasSuffix(localName) && document.expandedName(for: node) == self
25+
}
26+
}
27+
28+
29+
public protocol AttributeName: Sendable {
30+
func requestQualifiedName(in node: Node) -> String
31+
func qualifiedName(in: Node) -> String?
32+
}
33+
34+
extension String: AttributeName {
35+
public func requestQualifiedName(in node: Node) -> String {
36+
self
37+
}
38+
39+
public func qualifiedName(in: Node) -> String? {
40+
self
41+
}
42+
}
43+
44+
extension ExpandedName: AttributeName {
45+
public func requestQualifiedName(in node: Node) -> String {
46+
requestQualifiedAttributeName(for: node)
47+
}
48+
49+
public func qualifiedName(in node: Node) -> String? {
50+
if let match = qualifiedAttributeName(in: node) {
51+
return match
52+
} else if let placeholder = node.pendingNameRecord?.attributes[self] {
53+
// Namespace not in scope; try pending placeholder
54+
return placeholder
55+
} else {
56+
return nil
57+
}
58+
}
59+
}

0 commit comments

Comments
 (0)