File Coverage

blib/lib/XML/LibXML/LazyBuilder.pm
Criterion Covered Total %
statement 16 18 88.8
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 22 24 91.6


line stmt bran cond sub pod time code
1             package XML::LibXML::LazyBuilder;
2              
3 2     2   38654 use 5.008000;
  2         7  
  2         65  
4 2     2   9 use strict;
  2         4  
  2         88  
5 2     2   9 use warnings FATAL => 'all';
  2         20  
  2         100  
6              
7 2     2   11 use Carp ();
  2         3  
  2         37  
8 2     2   10 use Scalar::Util ();
  2         3  
  2         44  
9 2     2   1788 use XML::LibXML ();
  0            
  0            
10              
11             # consider using Exporter::Lite - djt
12             require Exporter;
13              
14             our @ISA = qw(Exporter);
15              
16             # Items to export into callers namespace by default. Note: do not export
17             # names by default without a very good reason. Use EXPORT_OK instead.
18             # Do not simply export all your public functions/methods/constants.
19              
20             # This allows declaration use XML::LibXML::LazyBuilder ':all';
21             # If you do not need this, moving things directly into @EXPORT or @EXPORT_OK
22             # will save memory.
23             our %EXPORT_TAGS = ( 'all' => [ qw(
24             DOM E P C D F DTD
25             ) ] );
26              
27             our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
28              
29             our @EXPORT = qw(
30             );
31              
32             our $VERSION = '0.08';
33              
34             # This is a map of all the DOM level 3 node names for
35             # non-element/attribute nodes. Note how there is no provision for
36             # processing instructions.
37             my %NODES = (
38             '#cdata-section' => 1,
39             '#comment' => 1,
40             '#document' => 1,
41             '#document-fragment' => 1,
42             '#text' => 1,
43             );
44              
45             # Note this is and will remain a stub until appropriate behaviour can
46             # be worked out.
47              
48             # (Perhaps a name of ?foo for processing instructions?)
49              
50             # nah, special methods for non-element nodes!
51              
52              
53              
54             # Preloaded methods go here.
55              
56              
57             # This predicate is an alternative to using UNIVERSAL::isa as a
58             # function (which is a no-no); it will return true if a blessed
59             # reference is derived from a built-in reference type.
60              
61             sub _is_really {
62             my ($obj, $type) = @_;
63             return unless defined $obj and ref $obj;
64             return Scalar::Util::blessed($obj) ? $obj->isa($type) : ref $obj eq $type;
65             }
66              
67             sub DOM ($;$$) {
68             my ($sub, $ver, $enc) = @_;
69              
70             my $dom = XML::LibXML::Document->new ($ver || "1.0", $enc || "utf-8");
71              
72             # this whole $dom $sub thing is cracking me up ;) -- djt
73             my $node = $sub->($dom);
74              
75             if (_is_really($node, 'XML::LibXML::DocumentFragment')) {
76             # "Appending a document fragment node to a document node not
77             # supported yet!", says XML::LibXML, so we work around it.
78              
79             for my $child ($node->childNodes) {
80             #warn $child->ownerDocument;
81             $child->unbindNode;
82             if ($child->nodeType == 1) {
83             if (my $root = $dom->documentElement) {
84             unless ($root->isSameNode($child)) {
85             Carp::croak("Trying to insert a second root element");
86             }
87             }
88             else {
89             $dom->setDocumentElement($child);
90             }
91             }
92             else {
93             $dom->appendChild($child);
94             }
95             }
96             }
97             elsif (_is_really($node, 'XML::LibXML::Element')) {
98             # NO-OP: Elements get attached to the root from inside the E
99             # function so it can access the namespace map.
100             }
101             else {
102             $dom->appendChild($node);
103             }
104              
105             $dom;
106             }
107              
108             sub E ($;$@) {
109             my ($name, $attr, @contents) = @_;
110              
111             return sub {
112             my ($dom, $parent) = @_;
113              
114             # note, explicit namespace declarations in the attribute set
115             # are held separately from actual namespace mappings found
116             # from scanning the document.
117             my (%ns, %nsdecl, %attr, $elem, $prefix);
118              
119             # pull the namespace declarations out of the attribute set
120             if (_is_really($attr, 'HASH')) {
121             while (my ($n, $v) = each %$attr) {
122             if ($n =~ /^xmlns(?::(.*))?$/) {
123             $nsdecl{$1 || ''} = $v;
124             }
125             else {
126             $attr{$n} = $v;
127             }
128             }
129             }
130              
131             if (_is_really($name, 'XML::LibXML::Element')) {
132             # throw an exception if the element is not bound to a
133             # document, which itself should become our new $dom
134             Carp::croak("The supplied element must be bound to a document")
135             unless $dom = $name->ownerDocument;
136              
137             # and of course $name is our new $elem
138             $elem = $name;
139             $name = $elem->nodeName;
140             $prefix = $elem->prefix || '';
141              
142             # then we don't need to scan the document for namespaces,
143             # but we probably should set it for attributes
144             %ns = map { $elem->lookupNamespacePrefix($_) || '' => $_ }
145             $elem->getNamespaces;
146             }
147             elsif (my $huh = ref $name) {
148             Carp::croak("Expected an XML::LibXML::Element; got $huh instead");
149             }
150             else {
151             # $name is a string
152             ($prefix) = ($name =~ /^(?:([^:]+):)?(.*)$/);
153             $prefix ||= '';
154              
155             # XXX what happens if $name isn't a valid QName?
156              
157             $elem = $dom->createElement($name);
158              
159             # check for a document element so we can find existing namespaces
160             if ($parent ||= $dom->documentElement) {
161             # XXX this is naive
162             for my $node ($parent->findnodes('namespace::*')) {
163             $ns{$node->declaredPrefix || ''} = $node->declaredURI;
164             }
165             }
166             else {
167             # do this here to make the tree walkable
168             $dom->setDocumentElement($elem);
169             }
170              
171             }
172              
173             # now do namespaces, overriding if necessary
174              
175             # first with the implicit mapping
176             if ($ns{$prefix}) {
177             $elem->setNamespace($ns{$prefix}, $prefix, 1);
178             }
179              
180             # then with the explicit declarations
181             for my $k (keys %nsdecl) {
182             # activate if the ns matches the prefix
183             $elem->setNamespace($nsdecl{$k}, $k, $k eq $prefix);
184             }
185              
186             # now smoosh the mappings together for the attributes
187             %ns = (%ns, %nsdecl);
188              
189             # NOW do the attributes
190             while (my ($n, $v) = each %attr) {
191             my ($pre, $loc) = ($n =~ /^(?:([^:]+):)?(.*)$/);
192              
193             # it'll probably mess up xpath queries if we explicitly
194             # add namespaces to non-prefixed attributes
195             if ($pre and my $nsuri = $ns{$pre}) {
196             $elem->setAttributeNS($nsuri, $n, $v);
197             }
198             else {
199             $elem->setAttribute($n, $v);
200             }
201             }
202              
203             # and finally child nodes
204             for my $child (@contents) {
205             if (_is_really($child, 'CODE')) {
206             $elem->appendChild ($child->($dom, $elem));
207             }
208             elsif (_is_really($child, 'XML::LibXML::Node')) {
209             # hey, why not?
210             $elem->appendChild($child);
211             }
212             elsif (my $huh = ref $child) {
213             Carp::croak
214             ("$huh is neither a CODE ref or an XML::LibXML::Node");
215             }
216             else {
217             $elem->appendTextNode ($child);
218             }
219             }
220              
221             $elem;
222             };
223             }
224              
225             # processing instruction
226             sub P ($;$@) {
227             my ($target, $attr, @text) = @_;
228              
229             return sub {
230             my $dom = shift;
231              
232             # copy, otherwise this will just keep packing it on if executed
233             # more than once
234             my @t = @text;
235              
236             # turn into k="v" convention
237             if (defined $attr) {
238             if (_is_really($attr, 'HASH')) {
239             my $x = join ' ',
240             map { sprintf '%s="%s"', $_, $attr->{$_} } keys %$attr;
241             unshift @t, $x;
242             }
243             else {
244             unshift @t, $attr;
245             }
246             }
247              
248             return $dom->createProcessingInstruction($target, join '', @t);
249             };
250             }
251              
252             # comment
253             sub C (;@) {
254             my @text = @_;
255              
256             return sub {
257             my $dom = shift;
258             $dom->createComment(join '', @text);
259             };
260             }
261              
262             # CDATA
263             sub D (;@) {
264             my @text = @_;
265              
266             return sub {
267             my $dom = shift;
268             $dom->createCDATASection(join '', @text);
269             };
270             }
271              
272             # document fragment
273             sub F (@) {
274             my @children = @_;
275              
276             return sub {
277             my $dom = shift;
278             my $frag = $dom->createDocumentFragment;
279             for my $child (@children) {
280             # same as E
281             if (_is_really($child, 'CODE')) {
282             $frag->appendChild($child->($dom));
283             }
284             elsif (_is_really($child, 'XML::LibXML::Node')) {
285             $frag->appendChild($child);
286             }
287             elsif (my $huh = ref $child) {
288             Carp::croak
289             ("$huh is neither a CODE ref or an XML::LibXML::Node");
290             }
291             else {
292             $frag->appendChild($dom->createTextNode($child));
293             }
294             }
295             $frag;
296             };
297             }
298              
299             sub DTD ($;$$) {
300             my ($name, $public, $system) = @_;
301              
302             return sub {
303             my $dom = shift;
304              
305             # must be an XS hiccup; can't just pass these in if they're undef
306             $dom->createExternalSubset($name, $public || undef, $system || undef);
307             };
308             }
309              
310             1;
311             __END__