Skip to content

Commit

Permalink
Improve namespace code
Browse files Browse the repository at this point in the history
  • Loading branch information
tomasf committed Jan 20, 2025
1 parent 0d9254d commit 57a85a4
Show file tree
Hide file tree
Showing 7 changed files with 162 additions and 78 deletions.
4 changes: 4 additions & 0 deletions Sources/Nodal/Document/Document+NodeObjects.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ internal extension Document {
}

static let deletedNodesUserInfoKey = "Nodal.DeletedNodes"

// Ideally, we'd use the object parameter on NotificationCenter to filter on document,
// but swift-corelibs-foundation seems to require that the class inherits from NSObject
// for that to work, so we use this as a workaround.
static let documentUserInfoKey = "Nodal.Document"

func sendNoteDeletionNotification(for nodes: Set<pugi.xml_node>) {
Expand Down
14 changes: 14 additions & 0 deletions Sources/Nodal/Document/Document+PendingNameRecords.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@ internal extension Document {
pendingNamespaceRecords[element.nodePointer]
}

func expandedName(for elementNode: pugi.xml_node) -> ExpandedName {
let qName = String(cString: elementNode.name())
if PendingNameRecord.qualifiedNameIndicatesPending(qName),
let record = pendingNamespaceRecords[elementNode.internal_object()],
let name = record.elementName {
return name
}

return ExpandedName(
namespaceName: elementNode.namespaceName(for: qName.qNamePrefix),
localName: qName.qNameLocalName
)
}

func addPendingNameRecord(for element: Element) -> PendingNameRecord {
let record = PendingNameRecord(element: element)
pendingNamespaceRecords[element.nodePointer] = record
Expand Down
15 changes: 8 additions & 7 deletions Sources/Nodal/Element/Element+Elements.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ public extension Element {
///
/// - Returns: An array of all child nodes that are elements.
var elements: [Element] {
childNodes(ofType: pugi.node_element).map { document.element(for: $0) }
childNodes(ofType: pugi.node_element)
.map { document.element(for: $0) }
}

/// Retrieves the first child element with the specified name.
Expand Down Expand Up @@ -46,11 +47,12 @@ public extension Element {
/// - Parameter targetName: The expanded name (including local name and optional namespace) of the elements to retrieve.
/// - Returns: An array of child elements matching the expanded name.
subscript(elements targetName: ExpandedName) -> [Element] {
let candidateElements = childNodes.filter { $0.type() == pugi.node_element && String(cString: $0.name()).hasSuffix(targetName.localName) }
return candidateElements.compactMap {
let element = document.element(for: $0)
return element.expandedName == targetName ? element : nil
let candidates = childNodes.filter {
$0.type() == pugi.node_element && String(cString: $0.name()).hasSuffix(targetName.localName)
}
return candidates.filter {
document.expandedName(for: $0) == targetName
}.map { document.element(for: $0) }
}

/// Retrieves all child elements with the specified local name and namespace URI.
Expand All @@ -70,8 +72,7 @@ public extension Element {
subscript(element targetName: ExpandedName) -> Element? {
let match = childNodes.first(where: {
guard $0.type() == pugi.node_element && String(cString: $0.name()).hasSuffix(targetName.localName) else { return false }
let element = document.element(for: $0)
return element.expandedName == targetName
return document.expandedName(for: $0) == targetName
})
return if let match { document.element(for: match) } else { nil }
}
Expand Down
75 changes: 5 additions & 70 deletions Sources/Nodal/Element/Element+Namespaces.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,80 +19,19 @@ internal extension Element {
]

var explicitNamespacesInScope: NamespaceBindings {
var namespaces: NamespaceBindings = [:]
var node: pugi.xml_node = self.node
while !node.empty() {
var attribute = node.first_attribute()
while !attribute.empty() {
let name = String(cString: attribute.name())
if name.hasPrefix("xmlns") {
let prefix = name == "xmlns" ? nil : name.qNameLocalName
if namespaces[prefix] == nil {
namespaces[prefix] = String(cString: attribute.value())
}
}
attribute = attribute.next_attribute()
}
node = node.parent()
}
return namespaces
node.explicitNamespacesInScope
}

func prefix(for namespaceName: String) -> String? {
if namespaceName == "http://www.w3.org/XML/1998/namespace" { return "xml" }
if namespaceName == "http://www.w3.org/2000/xmlns/" { return "xmlns" }

var node: pugi.xml_node = self.node
while !node.empty() {
var attribute = node.first_attribute()
while !attribute.empty() {
let name = String(cString: attribute.name())
if name.hasPrefix("xmlns"), String(cString: attribute.value()) == namespaceName {
return name.count == 5 ? "" : name.qNameLocalName
}
attribute = attribute.next_attribute()
}
node = node.parent()
}
return nil
node.prefix(for: namespaceName)
}

func nonDefaultPrefix(for namespaceName: String) -> String? {
if namespaceName == "http://www.w3.org/XML/1998/namespace" { return "xml" }
if namespaceName == "http://www.w3.org/2000/xmlns/" { return "xmlns" }

var node: pugi.xml_node = self.node
while !node.empty() {
var attribute = node.first_attribute()
while !attribute.empty() {
let name = String(cString: attribute.name())
if name.hasPrefix("xmlns:"), String(cString: attribute.value()) == namespaceName {
return name.qNameLocalName
}
attribute = attribute.next_attribute()
}
node = node.parent()
}
return nil
node.nonDefaultPrefix(for: namespaceName)
}

func namespaceName(for prefix: String?) -> String? {
if prefix == "xml" { return "http://www.w3.org/XML/1998/namespace" }
if prefix == "xmlns" { return "http://www.w3.org/2000/xmlns/" }

let targetAttributeName = if let prefix { "xmlns:" + prefix } else { "xmlns" }
var node: pugi.xml_node = self.node
while !node.empty() {
var attribute = node.first_attribute()
while !attribute.empty() {
if String(cString: attribute.name()) == targetAttributeName {
return String(cString: attribute.value())
}
attribute = attribute.next_attribute()
}
node = node.parent()
}
return nil
node.namespaceName(for: prefix)
}
}

Expand Down Expand Up @@ -179,11 +118,7 @@ public extension Element {
/// - Returns: An `ExpandedName` containing the local name and namespace name.
var expandedName: ExpandedName {
get {
if PendingNameRecord.qualifiedNameIndicatesPending(name), let record = pendingNameRecord, let name = record.elementName {
return name
} else {
return ExpandedName(namespaceName: namespaceName, localName: localName)
}
document.expandedName(for: node)
}
set {
name = newValue.effectiveQualifiedElementName(for: self)
Expand Down
43 changes: 43 additions & 0 deletions Sources/Nodal/Node/AncestorAttributeSequence.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import Foundation
import pugixml

internal struct AncestorAttributeSequence: Sequence, IteratorProtocol {
private var node: pugi.xml_node
private var attribute: pugi.xml_attribute
private var current: pugi.xml_attribute? = nil

init(target: pugi.xml_node) {
node = target
attribute = node.first_attribute()
}

init(target: Element) {
self.init(target: target.node)
}

mutating func next() -> pugi.xml_attribute? {
while attribute.empty() {
node = node.parent()
if node.empty() {
return nil
}
attribute = node.first_attribute()
}

let result = attribute
attribute = attribute.next_attribute()
return result
}
}

internal extension Element {
var ancestorAttributes: AncestorAttributeSequence {
AncestorAttributeSequence(target: self)
}
}

internal extension pugi.xml_node {
var ancestorAttributes: AncestorAttributeSequence {
AncestorAttributeSequence(target: self)
}
}
58 changes: 58 additions & 0 deletions Sources/Nodal/Pugi.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,61 @@ internal extension pugi.xpath_node_set {
(0..<size()).map { self[$0] }
}
}

internal extension pugi.xml_node {
func namespaceName(for prefix: String?) -> String? {
if prefix == "xml" { return "http://www.w3.org/XML/1998/namespace" }
if prefix == "xmlns" { return "http://www.w3.org/2000/xmlns/" }

let targetAttributeName = if let prefix { "xmlns:" + prefix } else { "xmlns" }

for attribute in ancestorAttributes {
if String(cString: attribute.name()) == targetAttributeName {
return String(cString: attribute.value())
}
}
return nil
}

func nonDefaultPrefix(for namespaceName: String) -> String? {
if namespaceName == "http://www.w3.org/XML/1998/namespace" { return "xml" }
if namespaceName == "http://www.w3.org/2000/xmlns/" { return "xmlns" }

for attribute in ancestorAttributes {
let name = String(cString: attribute.name())
if name.hasPrefix("xmlns:"), String(cString: attribute.value()) == namespaceName {
return name.qNameLocalName
}
}
return nil
}

func prefix(for namespaceName: String) -> String? {
if namespaceName == "http://www.w3.org/XML/1998/namespace" { return "xml" }
if namespaceName == "http://www.w3.org/2000/xmlns/" { return "xmlns" }

for attribute in ancestorAttributes {
let name = String(cString: attribute.name())
if name.hasPrefix("xmlns"), String(cString: attribute.value()) == namespaceName {
return name.count == 5 ? "" : name.qNameLocalName
}
}
return nil
}

var explicitNamespacesInScope: NamespaceBindings {
var namespaces: NamespaceBindings = [:]

for attribute in ancestorAttributes {
let name = String(cString: attribute.name())
if name.hasPrefix("xmlns") {
let prefix = name == "xmlns" ? nil : name.qNameLocalName
if namespaces[prefix] == nil {
namespaces[prefix] = String(cString: attribute.value())
}
}
}

return namespaces
}
}
31 changes: 30 additions & 1 deletion Sources/Tests/Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Testing

struct Tests {
@Test
func namespaceResolution() {
func deferredNamespaceResolution() {
let document = Document()
let root = document.makeDocumentElement(name: "root")

Expand Down Expand Up @@ -38,6 +38,35 @@ struct Tests {
#expect(a[attribute: "aa:c"] == "bar")
}

@Test
func expandedNames() throws {
let doc = Document()
let root = doc.makeDocumentElement(name: "root")
let nn1 = "namespace1"
let nn2 = "namespace2"

root.declareNamespace(nn2, forPrefix: "n2")

let en1 = ExpandedName(namespaceName: nn1, localName: "local1")
let en2 = ExpandedName(namespaceName: nn2, localName: "local2")

let e1 = root.appendElement(en1)
#expect(e1.expandedName == en1)

#expect(root[elements: en1].count == 1)
#expect(root[elements: en2].count == 0)

let e2 = root.appendElement(en2)
#expect(e2.expandedName == en2)
#expect(root[elements: en2].count == 1)

root.declareNamespace(nn1, forPrefix: "n1")
#expect(e1.expandedName == en1)

#expect(doc.pendingNameRecordCount == 0)
_ = try doc.xmlData() // Should not throw
}

// addChild should return nil if the type of node can't be added to that parent
@Test
func addChild() throws {
Expand Down

0 comments on commit 57a85a4

Please sign in to comment.