File Coverage

blib/lib/Config/Generator/XML.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             #+##############################################################################
2             # #
3             # File: Config/Generator/XML.pm #
4             # #
5             # Description: Config::Generator XML support #
6             # #
7             #-##############################################################################
8              
9             #
10             # module definition
11             #
12              
13             package Config::Generator::XML;
14 1     1   22505 use strict;
  1         1  
  1         25  
15 1     1   4 use warnings;
  1         1  
  1         77  
16             our $VERSION = "1.0";
17             our $REVISION = sprintf("%d.%02d", q$Revision: 1.18 $ =~ /(\d+)\.(\d+)/);
18              
19             #
20             # used modules
21             #
22              
23 1     1   402 use No::Worries::Die qw(dief);
  1         2544  
  1         5  
24 1     1   68 use No::Worries::Export qw(export_control);
  1         2  
  1         3  
25 1     1   55 use Params::Validate qw(validate validate_pos :types);
  1         2  
  1         136  
26 1     1   844 use XML::Parser qw();
  0            
  0            
27              
28             #
29             # constants
30             #
31              
32             use constant TAB => " ";
33              
34             #
35             # return a hash representing a blank line (i.e. no name and no body)
36             #
37              
38             sub xml_blank () {
39             return({});
40             }
41              
42             #
43             # return a hash representing an XML comment (i.e. no name and a string body)
44             #
45              
46             my @xml_comment_options = (
47             { type => SCALAR },
48             );
49              
50             sub xml_comment ($) {
51             my($string) = validate_pos(@_, @xml_comment_options);
52              
53             return({ body => $string });
54             }
55              
56             #
57             # return a hash representing an XML element
58             #
59              
60             my @xml_element_options = (
61             { type => SCALAR },
62             { type => ARRAYREF | HASHREF | UNDEF, optional => 1 },
63             );
64              
65             sub xml_element ($;$@) {
66             my($name, $attr, @body) = validate_pos(@_, @xml_element_options,
67             @_ == 3 ? ({ type => SCALAR | HASHREF })
68             : ({ type => HASHREF }) x (@_ - 2)
69             );
70             my(%elt);
71              
72             $elt{name} = $name;
73             $elt{attr} = $attr if $attr;
74             if (@body) {
75             if (@body == 1 and not ref($body[0])) {
76             $elt{body} = $body[0];
77             } else {
78             $elt{body} = \@body;
79             }
80             }
81             return(\%elt);
82             }
83              
84             #
85             # parse the given string that must contain valid XML
86             #
87              
88             my @xml_parse_options = (
89             { type => SCALAR },
90             );
91              
92             sub xml_parse ($) {
93             my($string) = validate_pos(@_, @xml_parse_options);
94             my($parser);
95              
96             $parser = XML::Parser->new(Style => __PACKAGE__ . "::Parser");
97             return($parser->parse($string));
98             }
99              
100             #
101             # escape a string so that it can be used outside CDATA
102             #
103              
104             sub _escape ($) {
105             my($string) = @_;
106              
107             $string =~ s/&/&/g;
108             $string =~ s/
109             $string =~ s/>/>/g;
110             return($string);
111             }
112              
113             #
114             # return the list of attributes of an XML element
115             #
116              
117             sub _attrs ($%) {
118             my($elt, %option) = @_;
119             my($eltname, @names, @attrs);
120              
121             return() unless $elt->{attr};
122             $eltname = $elt->{name};
123             if (ref($elt->{attr}) eq "HASH") {
124             # given as hash for convenience
125             if ($option{sort}{$eltname}) {
126             @names = $option{sort}{$eltname}->(keys(%{ $elt->{attr} }));
127             } else {
128             @names = sort(keys(%{ $elt->{attr} }));
129             }
130             foreach my $name (@names) {
131             push(@attrs, $name . '="' . $elt->{attr}{$name} . '"');
132             }
133             } elsif (ref($elt->{attr}) eq "ARRAY") {
134             # given as name=value array for full control
135             @attrs = @{ $elt->{attr} };
136             } else {
137             dief("unexpected XML attribute: %s", $elt->{attr});
138             }
139             return(@attrs);
140             }
141              
142             #
143             # return a string representing a blank line or an XML comment
144             #
145              
146             sub _strspc ($$) {
147             my($string, $indent) = @_;
148             my($xml);
149              
150             return("\n") unless defined($string);
151             $xml = $indent . "\n";
162             return($xml);
163             }
164              
165             #
166             # return a string representing an XML element (private)
167             #
168              
169             sub _strelt ($%);
170             sub _strelt ($%) {
171             my($elt, %option) = @_;
172             my($eltname, $indent, $xml, @attrs, $line, $sep);
173              
174             $eltname = $elt->{name};
175             $indent = TAB() x $option{indent};
176             return(_strspc($elt->{body}, $indent)) unless $eltname;
177             $xml = $indent . "<" . $eltname;
178             @attrs = _attrs($elt, %option);
179             if (@attrs) {
180             @attrs = map(_escape($_), @attrs);
181             $line = $option{line};
182             $line = 1 if $option{split}{$eltname};
183             if ($line and length("$indent<$eltname @attrs>\n") > $line) {
184             $sep = "\n$indent";
185             @attrs = (map(TAB() . $_, @attrs), "");
186             } else {
187             $sep = " ";
188             }
189             $xml .= join($sep, "", @attrs);
190             }
191             if (defined($elt->{body}) and not ref($elt->{body})) {
192             # string body
193             $xml .= ">" . $elt->{body} . "\n";
194             } elsif ($elt->{body} and @{ $elt->{body} }) {
195             # structure body
196             $xml .= ">\n";
197             $option{indent}++;
198             foreach my $child (@{ $elt->{body} }) {
199             dief("unexpected XML child: %s", $child)
200             unless ref($child) eq "HASH";
201             $xml .= _strelt($child, %option);
202             }
203             $xml .= $indent . "\n";
204             } else {
205             # no body
206             $xml .= "/>\n";
207             }
208             return($xml);
209             }
210              
211             #
212             # return a string representing an XML element (public)
213             #
214              
215             my %xml_string_options = (
216             indent => { optional => 1, type => SCALAR, regex => qr/^\d+$/ },
217             line => { optional => 1, type => SCALAR, regex => qr/^\d+$/ },
218             sort => { optional => 1, type => HASHREF },
219             split => { optional => 1, type => HASHREF },
220             );
221              
222             sub xml_string ($@) {
223             my($elt, %option);
224              
225             $elt = shift(@_);
226             dief("unexpected XML element: %s", $elt)
227             unless ref($elt) eq "HASH";
228             %option = validate(@_, \%xml_string_options) if @_;
229             $option{indent} ||= 0;
230             return(_strelt($elt, %option));
231             }
232              
233             #
234             # wrap the given XML elements into nested elements with no attributes
235             #
236              
237             sub xml_wrap (@);
238             sub xml_wrap (@) {
239             my($name, @list) = @_;
240              
241             dief("no XML elements given to xml_wrap()!") unless @list;
242             return(xml_element($name, undef, xml_wrap(@list))) if ref($list[0]) eq "";
243             return(xml_element($name, undef, @list));
244             }
245              
246             #
247             # export control
248             #
249              
250             sub import : method {
251             my($pkg, %exported);
252              
253             $pkg = shift(@_);
254             grep($exported{$_}++, map("xml_$_",
255             qw(blank comment element parse string wrap)));
256             export_control(scalar(caller()), $pkg, \%exported, @_);
257             }
258              
259             #+##############################################################################
260             # #
261             # XML::Parser-compatible handlers #
262             # #
263             #-##############################################################################
264              
265             package Config::Generator::XML::Parser;
266             use strict;
267             use warnings;
268              
269             #
270             # used modules
271             #
272              
273             use No::Worries::Die qw(dief);
274              
275             #
276             # handlers
277             #
278              
279             sub Init ($) {
280             my($parser) = @_;
281              
282             $parser->{TopLevel} = $parser->{Current} = [];
283             $parser->{Stack} = [];
284             }
285              
286             sub Start ($$%) {
287             my($parser, $tag, %attr) = @_;
288             my($elt);
289              
290             $elt = {
291             name => $tag,
292             attr => \%attr,
293             body => [],
294             };
295             push(@{ $parser->{Stack} }, $parser->{Current});
296             push(@{ $parser->{Current} }, $elt);
297             $parser->{Current} = $elt->{body};
298             }
299              
300             sub End ($$) {
301             my($parser, $tag) = @_;
302             my($elt);
303              
304             $elt = $parser->{Stack}[-1][-1];
305             # FIXME: forbid multi-line body?
306             if (@{ $elt->{body} } == 0) {
307             # no body
308             delete($elt->{body});
309             } elsif (@{ $elt->{body} } == 1 and not ref($elt->{body}[0])) {
310             # string body
311             $elt->{body} = $elt->{body}[0];
312             } elsif (@{ $elt->{body} } > 1) {
313             # enforce body consistency
314             foreach my $chunk (@{ $elt->{body} }) {
315             dief("unexpected text: %s", $chunk) unless ref($chunk);
316             }
317             }
318             $parser->{Current} = pop(@{ $parser->{Stack} });
319             }
320              
321             sub Char ($$) {
322             my($parser, $text) = @_;
323              
324             return if $text =~ /^\s*$/;
325             $text =~ s/^\s+//;
326             $text =~ s/\s+$//;
327             dief("unexpected multi-line text: %s", $text) if $text =~ /\n/;
328             push(@{ $parser->{Current} }, $text);
329             }
330              
331             sub Comment ($$) {
332             my($parser, $text) = @_;
333             my(@lines);
334              
335             $text =~ s/^\s+//;
336             $text =~ s/\s+$//;
337             foreach my $line (split(/\n/, $text)) {
338             $line =~ s/^\s+//;
339             $line =~ s/\s+$//;
340             push(@lines, $line);
341             }
342             push(@{ $parser->{Current} }, { body => join("\n", @lines) });
343             }
344              
345             sub Final ($) {
346             my($parser) = @_;
347              
348             delete($parser->{Current});
349             delete($parser->{Stack});
350             return(@{ delete($parser->{TopLevel}) });
351             }
352              
353             1;
354              
355             __DATA__