@@ -2,29 +2,75 @@ class XML::Node
2
2
LOOKS_LIKE_XPATH = /^(\.\/ |\/ |\.\. |\. $) /
3
3
4
4
# Every Node must keep a reference to its document Node. To keep things
5
- # simple, a document Node merely references itself.
5
+ # simple, a document Node merely references itself. An unlinked node must
6
+ # still reference its original document Node until adopted into another
7
+ # document's tree.
6
8
@document : Node
7
9
8
- # Unlinked Nodes must still reference its original document Node. They don't
9
- # appear in the document's tree anymore, and they won't be freed along with
10
- # the document, but we still need to access some data on the document's Node,
11
- # and thus need to keep it alive.
10
+ # Remembers subtree nodes. Avoids allocating a XML::Node twice for the same
11
+ # libxml node. This allows to keep centralized information about the nodes so
12
+ # the finalizer won't try to access the libxml node to detect some state
13
+ # —which is unsafe because it may have been freed already, and won't try to
14
+ # free an unlinked node twice (or leak unlinked libxml nodes).
15
+ protected getter! cache : Hash (UInt64 , UInt64 )?
16
+
17
+ # Unlinked Nodes must still reference their original document Node. They don't
18
+ # appear in the document's tree anymore and thus won't be freed along with the
19
+ # document, but the libxml node still referes to the libxml doc and thus need
20
+ # to keep the document alive.
12
21
@unlinked = false
13
22
14
23
# :nodoc:
15
- def initialize (node : LibXML ::Doc * , @errors : Array (XML ::Error )? = nil )
16
- @node = node.as(LibXML ::Node * )
24
+ #
25
+ # Allocates a XML::Node for a libxml document node once, so we don't finalize
26
+ # a document twice. We can store the pointer right into the libxml struct
27
+ # because the XML::Node lives as long as the libxml doc.
28
+ def self.new (doc : LibXML ::Doc * , errors : Array (Error )? = nil )
29
+ if ptr = doc.value._private
30
+ ptr.as(Node )
31
+ else
32
+ new(doc_: doc, errors_: errors)
33
+ end
34
+ end
35
+
36
+ # :nodoc:
37
+ def self.new (node : LibXML ::Node * , document : self ) : self
38
+ # should never happen, but just in case
39
+ return document if node == document.@node
40
+
41
+ cache = document.cache
42
+ if (addr = cache[node.address]?) && addr != 0
43
+ return Pointer (Void ).new(addr).as(Node )
44
+ end
45
+
46
+ this = new(node_: node, document_: document)
47
+ cache[node.address] = this.as(Void * ).address
48
+ this
49
+ end
50
+
51
+ # :nodoc:
52
+ @[Deprecated (" Use XML::Node.new(node, document) instead." )]
53
+ def self.new (node : LibXML ::Node * ) : self
54
+ new(node, new(node.value.doc))
55
+ end
56
+
57
+ private def initialize (* , doc_ : LibXML ::Doc * , errors_ : Array (Error )?)
58
+ @node = doc_.as(LibXML ::Node * )
59
+ @errors = errors_
60
+ @cache = Hash (UInt64 , UInt64 ).new
17
61
@document = uninitialized Node
18
62
@document = self
63
+ doc_.value._private = self .as(Void * )
19
64
end
20
65
21
- # :nodoc:
22
- def initialize (@node : LibXML ::Node * , @document : Node )
66
+ private def initialize (* , node_ : LibXML ::Node * , document_ : self )
67
+ @node = node_.as(LibXML ::Node * )
68
+ @document = document_
23
69
end
24
70
25
71
# :nodoc:
26
72
def finalize
27
- if document?
73
+ if @ document == self
28
74
# free the document, which will recursively free the DOM tree, NS, ...
29
75
LibXML .xmlFreeDoc(@node .as(LibXML ::Doc * ))
30
76
elsif @unlinked
@@ -575,8 +621,14 @@ class XML::Node
575
621
576
622
# Removes the node from the XML document.
577
623
def unlink : Nil
578
- LibXML .xmlUnlinkNode(self )
624
+ return if @unlinked
625
+
579
626
@unlinked = true
627
+ LibXML .xmlUnlinkNode(self )
628
+ end
629
+
630
+ def unlinked ? : Bool
631
+ @unlinked
580
632
end
581
633
582
634
# Returns `true` if this is an xml Document node.
0 commit comments