Skip to content

Commit

Permalink
Remove node object cache and PointerToObjectMap
Browse files Browse the repository at this point in the history
  • Loading branch information
tomasf committed Jan 20, 2025
1 parent f219ab7 commit 4b042bc
Show file tree
Hide file tree
Showing 14 changed files with 160 additions and 182 deletions.
57 changes: 13 additions & 44 deletions Sources/Nodal/Document/Document+NodeObjects.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,52 +2,9 @@ import Foundation
import pugixml

internal extension Document {
// Associate an Element object with an element node
func setElementObject(_ element: Element, forNode node: pugi.xml_node) {
objectDirectory[node.internal_object()] = element
}

// Fetches any already existing Element object for a given element node
func existingElementObject(forNode node: pugi.xml_node) -> Element? {
objectDirectory[node.internal_object()]
}

// Create a new Element object for a given node
// This can be used directly for newly created nodes, to avoid checking the map table first
func createElementObject(forNode node: pugi.xml_node) -> Element {
let new = Element(owningDocument: self, node: node)
setElementObject(new, forNode: node)
//print("Created object. Count: \(objectDirectory.count)")
return new
}

func invalidateElementObject(_ element: Element) {
objectDirectory[element.nodePointer] = nil
element.invalidate()
}

func invalidateElementObjects(withinTree ancestor: Element, excludingTarget: Bool = false) {
ancestor.traverse { pugiNode, _ in
if pugiNode.type() == pugi.node_element {
self.existingElementObject(forNode: pugiNode)?.invalidate()
self.objectDirectory[pugiNode.internal_object()] = nil
}
return true
}
if !excludingTarget {
objectDirectory[ancestor.nodePointer] = nil
ancestor.invalidate()
}
}

// Get an Element object for an element node
// This returns an existing object if one exists; otherwise creates one
func element(for node: pugi.xml_node) -> Element {
if let existing = existingElementObject(forNode: node) {
return existing
} else {
return createElementObject(forNode: node)
}
Element(owningDocument: self, node: node)
}

// This gets a Node object for any node. Non-element objects are not reused.
Expand All @@ -63,4 +20,16 @@ internal extension Document {
func objectIfValid(_ node: pugi.xml_node) -> Node? {
node.empty() ? nil : object(for: node)
}

static let deletedNodesUserInfoKey = "Nodal.DeletedNodes"

func sendNoteDeletionNotification(for nodes: Set<pugi.xml_node>) {
NotificationCenter.default.post(name: .documentDidDeleteNodes, object: self, userInfo: [
Self.deletedNodesUserInfoKey: nodes
])
}
}

internal extension Notification.Name {
static let documentDidDeleteNodes = Notification.Name("Nodal.Document.didDeleteNodes")
}
7 changes: 4 additions & 3 deletions Sources/Nodal/Document/Document+PendingNameRecords.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,18 @@ internal extension Document {

func removePendingNameRecords(withinTree ancestor: Element, excludingTarget: Bool = false) {
let nodePointer = ancestor.nodePointer
let keys = pendingNamespaceRecords.contents.filter { node, record in
let keys = pendingNamespaceRecords.filter { node, record in
if excludingTarget && node == nodePointer {
return false
}
return record.ancestors.contains(ancestor.node)
}.map(\.key)
pendingNamespaceRecords.removeObjects(forKeys: keys)

for key in keys { pendingNamespaceRecords[key] = nil }
}

func pendingNameRecords(forDescendantsOf parent: Element) -> [(Element, PendingNameRecord)] {
pendingNamespaceRecords.contents.compactMap {
pendingNamespaceRecords.compactMap {
$1.belongsToTree(parent) ? (element(for: .init($0)), $1) : nil
}
}
Expand Down
5 changes: 2 additions & 3 deletions Sources/Nodal/Document/Document.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ import Bridge
/// Represents an XML document node, providing methods for working with the document structure and serialization.
public class Document: Node {
internal var pugiDocument = pugi.xml_document()
internal var objectDirectory = PointerToObjectMap<Element>(strength: .weak)
internal var pendingNamespaceRecords = PointerToObjectMap<PendingNameRecord>(strength: .strong)
internal var pendingNamespaceRecords: [OpaquePointer: PendingNameRecord] = [:]

internal required init(owningDocument: Document?, node: pugi.xml_node) {
super.init(owningDocument: nil, node: pugiDocument.asNode)
Expand All @@ -20,7 +19,7 @@ public class Document: Node {
///
/// - Note: This property helps identify undeclared namespaces that need to be resolved to generate XML output.
public var undeclaredNamespaceNames: Set<String> {
Set(pendingNamespaceRecords.contents.flatMap(\.value.namespaceNames))
Set(pendingNamespaceRecords.flatMap(\.value.namespaceNames))
}

private func save(encoding: String.Encoding = .utf8,
Expand Down
1 change: 0 additions & 1 deletion Sources/Nodal/Element/Element+Attributes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ internal extension Element {
}

func declaredNamespacesDidChange() {
invalidateNamespaceCache()
let namespaces = declaredNamespaces
for (element, record) in document.pendingNameRecords(forDescendantsOf: self) {
if record.attemptResolution(for: element, with: namespaces) {
Expand Down
2 changes: 1 addition & 1 deletion Sources/Nodal/Element/Element+Elements.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public extension Element {
/// - Returns: The newly created child element.
@discardableResult
func appendElement(_ name: String) -> Element {
let element = document.createElementObject(forNode: node.append_child(pugi.node_element))
let element = document.element(for: node.append_child(pugi.node_element))
element.name = name
return element
}
Expand Down
10 changes: 4 additions & 6 deletions Sources/Nodal/Element/Element+ExpandedAttributes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,15 @@ public extension Element {
/// - Note: Setting this property replaces all existing attributes with the new ones, preserving the specified order.
var orderedAttributes: [(name: ExpandedName, value: String)] {
get {
let namespaces = namespacesInScope
return nodeAttributes.map {(
ExpandedName(effectiveQualifiedAttributeName: String(cString: $0.name()), in: self, using: namespaces),
ExpandedName(effectiveQualifiedAttributeName: String(cString: $0.name()), in: self),
String(cString: $0.value())
)}
}
set {
let namespaces = namespacesInScope
node.remove_attributes()
for (name, value) in newValue {
let qName = name.effectiveQualifiedAttributeName(for: self, using: namespaces)
let qName = name.effectiveQualifiedAttributeName(for: self)
var attr = node.append_attribute(qName)
attr.set_value(value)
}
Expand Down Expand Up @@ -53,7 +51,7 @@ public extension Element {
subscript(attribute name: ExpandedName) -> String? {
get {
let qName: String
if let match = name.qualifiedAttributeName(using: namespacesInScope) {
if let match = name.qualifiedAttributeName(in: self) {
qName = match
} else if let placeholder = pendingNameRecord?.attributes[name] {
// Namespace not in scope; try pending placeholder
Expand All @@ -65,7 +63,7 @@ public extension Element {
return self[attribute: qName]
}
set {
let qName = name.effectiveQualifiedAttributeName(for: self, using: namespacesInScope)
let qName = name.effectiveQualifiedAttributeName(for: self)
self[attribute: qName] = newValue
}
}
Expand Down
93 changes: 74 additions & 19 deletions Sources/Nodal/Element/Element+Namespaces.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,81 @@ internal extension Element {
]

var explicitNamespacesInScope: NamespaceBindings {
var namespaces = parentElement?.explicitNamespacesInScope ?? [:]

for pugiAttribute in nodeAttributes {
let name = String(cString: pugiAttribute.name())
if name == "xmlns" {
namespaces[nil] = String(cString: pugiAttribute.value())
} else if name.hasPrefix("xmlns:") {
let prefix = String(name.dropFirst(6))
namespaces[prefix] = String(cString: pugiAttribute.value())
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
}

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
}

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
}

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
}
}

public extension Element {
Expand Down Expand Up @@ -82,24 +143,18 @@ public extension Element {
/// - Returns: A dictionary where the keys are namespace prefixes (or `nil` for the default namespace),
/// and the values are the corresponding namespace names (URIs).
var namespacesInScope: NamespaceBindings {
if let cachedNamespacesInScope {
return cachedNamespacesInScope
}

var namespaces = explicitNamespacesInScope
for (key, value) in Self.fixedNamespaces {
namespaces[key] = value
}

cachedNamespacesInScope = namespaces
return namespaces
}

/// The default namespace name (URI) for this element.
///
/// - Returns: The URI associated with the default namespace (`xmlns`) in this scope, or `nil` if no default namespace is declared.
var defaultNamespaceName: String? {
namespacesInScope[nil]
namespaceName(for: nil)
}

/// The local name of this element's qualified name, excluding any prefix.
Expand All @@ -116,7 +171,7 @@ public extension Element {
///
/// - Returns: The namespace URI for the prefix, or `nil` if the prefix is not bound to a namespace.
var namespaceName: String? {
namespacesInScope[prefix]
namespaceName(for: prefix)
}

/// The expanded name of this element, including its namespace name (URI) and local name.
Expand All @@ -131,7 +186,7 @@ public extension Element {
}
}
set {
name = newValue.effectiveQualifiedElementName(for: self, using: namespacesInScope)
name = newValue.effectiveQualifiedElementName(for: self)
}
}

Expand Down
16 changes: 1 addition & 15 deletions Sources/Nodal/Element/Element.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,4 @@ 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 {
internal var cachedNamespacesInScope: [String?: String]?

internal func invalidateNamespaceCache() {
cachedNamespacesInScope = nil
traverse { node, _ in
if node.type() == pugi.node_element, let element = self.document.existingElementObject(forNode: node) {
element.cachedNamespacesInScope = nil
}
return true
}
}
}


public class Element: Node {}
4 changes: 0 additions & 4 deletions Sources/Nodal/Element/PendingNameRecord.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,6 @@ internal class PendingNameRecord {
return names
}

func attemptResolution(for element: Element) -> Bool {
attemptResolution(for: element, with: element.namespacesInScope)
}

// Returns true if the element is now completely resolved and the record can be removed
func attemptResolution(for element: Element, with namespaceBindings: NamespaceBindings) -> Bool {
let namespaces = namespaceBindings.nameToPrefixMapping
Expand Down
Loading

0 comments on commit 4b042bc

Please sign in to comment.