File Coverage

blib/lib/Data/PropertyList.pm
Criterion Covered Total %
statement 129 156 82.6
branch 55 100 55.0
condition 12 23 52.1
subroutine 15 17 88.2
pod 2 2 100.0
total 213 298 71.4


line stmt bran cond sub pod time code
1             ### Data::PropertyList - Convert arbitrary objects to/from strings.
2              
3             ### Copyright 1996, 1997, 1998 Evolution Online Systems, Inc.
4             # You may use this software for free under the terms of the Artistic License
5              
6             ### Change History
7             # 1998-12-17 Minor doc cleanup; added makefile and tests for distribution.
8             # 1998-10-05 Tweaked output spacing for single-line string arrays.
9             # 1998-10-05 Switched from use of String::Escape::add to direct hash access.
10             # 1998-07-23 Conventionalized POD, switched to yyyy.mm_dd version numbering.
11             # 1998-08-23 On further consideration, this really did belong in Data::*.
12             # 1998-06-11 Improved support for <
13             # 1998-05-07 Fixed problem with reading "0 = ..." lines in hashes.
14             # 1998-03-03 Replaced $r->class with ref($r) -Simon
15             # 1998-02-28 Initialized _parse_multiline $value to '' to run clean under -w.
16             # 1998-02-25 Version 1.00 - String::PropertyList
17             # 1998-02-25 Moved to String:: and @EXPORT_OK for CPAN distribution - jeremy
18             # 1998-01-28 Fixed variable name typo in _parse_array.
19             # 1998-01-11 Added rudimentary support for comments: full-line comments only
20             # 1998-01-02 Renamed package Data::PropertyList to Text::PropertyList -Simon
21             # 1997-12-08 Removed package Data::Types, use UNIVERSAL::isa instead. -Piglet
22             # 1997-11-19 Added loopback handling to astext; now Supress as XREF TO
23             # 1997-10-28 Updated to use new Text::Escape interface.
24             # 1997-10-21 Documentation cleanup.
25             # 1997-08-17 Moved string escape/unescape code into new Text::Escape. -Simon
26             # 1997-01-2? New fromDictionary parser -Eric
27             # 1997-01-14 New asDictionary function provides closer match to NeXT style.
28             # 1997-01-11 Cloned & cleaned for Inetics; moved I/O to file.pm. V3.0 -Simon
29             # 1996-10-29 Added append flag and trailing \n to write. -Piglet
30             # 1996-08-06 Partial fix for blessed data; treat as basic type. V2.05 -Simon
31             # 1996-07-13 Cleaned up flow, fixed headers.
32             # 1996-06-25 Wrote &write. V2.04 -EJ
33             # 1996-06-23 Converted from Perl 4 library to Perl 5 package. V2.03
34             # 1996-06-18 Iterative line parsing replaces raw recursion. V2.02
35             # 1996-06-15 Clean start with support for nested data structures. V2.01
36             # 1996-05-26 Support for =<< multiline values.
37             # 1996-05-08 Parse key-value pairs into a flat hash. Version 1. -Simon
38              
39             package Data::PropertyList;
40              
41             require 5.003;
42 3     3   23164 use strict;
  3         8  
  3         134  
43              
44 3     3   17 use vars qw( $VERSION @ISA @EXPORT_OK );
  3         5  
  3         233  
45             $VERSION = 1998.12_17;
46              
47 3     3   16 use Exporter;
  3         15  
  3         187  
48             push @ISA, qw( Exporter );
49             push @EXPORT_OK, qw( astext fromtext );
50              
51 3     3   20 use vars qw( $Separator );
  3         5  
  3         163  
52             $Separator = '.';
53              
54 3     3   3192 use String::Escape qw( qprintable unprintable );
  3         21697  
  3         737  
55             $String::Escape::Escapes{'astext'} = \&astext;
56             $String::Escape::Escapes{'fromtext'} = \&fromtext;
57              
58             ### Writer
59              
60 3     3   34 use vars qw( %DRefs %Supress $CurrentDRef $CurrentDepth );
  3         9  
  3         244  
61 3     3   14 use vars qw( $Indent $ShowClasses $ShowDRefs $Multiline $MaxItems );
  3         5  
  3         3570  
62              
63             # $string = astext($value_or_reference);
64             # $string = astext($value_or_reference, %options);
65             # Write out an object graph in NeXT property list format
66             # Numerous variables are localized, then we recurse.
67             sub astext {
68 23     23 1 2565 my $target = shift;
69 23         52 my %options = @_;
70              
71             # Options
72 23         38 local $CurrentDRef = '';
73 23         33 local $CurrentDepth = 0;
74            
75 23         29 local $Indent = 2;
76            
77 23 50       62 local $ShowClasses = $options{'-classes'} if (exists $options{'-classes'} );
78 23 100       59 local $ShowDRefs = $options{'-drefs'} if (exists $options{'-drefs'} );
79 23 50       87 local $Multiline = $options{'-multiline'} if (exists $options{'-multiline'});
80 23 100       54 local $MaxItems = $options{'-maxitems'} if (exists $options{'-maxitems'});
81            
82             # Working scope for this invokation of astext.
83 23         43 local %DRefs = ();
84 23         33 local %Supress = ();
85            
86 23         50 _astext( $target )
87             }
88              
89             # $string = _astext($referenceorvalue);
90             sub _astext {
91 571     571   903 my $target = shift;
92            
93             # Write out an "UNDEFINED" comment to signal undefined values;
94 571 50       1262 return '/* UNDEFINED */' if (not defined $target);
95            
96             # Write out escaped version of non-reference (string or number) values.
97 571 100       1094 if ( ! ref($target) ) {
98 557 50 33     1282 if ( $Multiline and $target =~ /\n.*?\n/ ) {
99 0 0       0 return "<
100             $target . ($target =~ /\n\Z/ ?'':"\n") .
101             " END_OF_TEXT_DELIMITER";
102             } else {
103 557         1408 return qprintable( $target );
104             }
105             }
106            
107             # If this is a reference an item written out elsewhere, write an XREF comment
108 14 0 66     70 return '/* CROSS-REFERENCE TO ' .
    50          
109             ( length($DRefs{$target}) ? $DRefs{$target} : 'ROOT' ) .' */'
110             if ( exists $DRefs{$target} and $Supress{$target} );
111            
112             # Store a relative DRef from the root to here, if we haven't already
113 14 100       50 $DRefs{$target} = $CurrentDRef if ( not exists $DRefs{$target});
114            
115             # We're going to show this item, so don't show it again in the future
116 14         28 $Supress{$target} ++ ;
117            
118             # Variable to hold the stringified form of $target.
119 14         18 my $result = '';
120            
121             # Write out DRef if $ShowDRefs is set
122 14 100 100     48 $result .= "/* DREF $CurrentDRef */ "
123             if ($ShowDRefs and length $CurrentDRef);
124            
125             # Any DRefs after this point are separated by dots.
126 14 100       36 local $CurrentDRef = $CurrentDRef . $Separator if ( length $CurrentDRef );
127            
128             # Write out class of item if it's blessed and $ShowClasses is set
129 14 0 33     34 $result .= "/* CLASS " . ref($target) . " */ " if ($ShowClasses and
      33        
130             ref($target) and (ref($target) !~ /\A(ARRAY|HASH|SCALAR|REF|CODE)\Z/));
131            
132 14 100 0     60 if ( UNIVERSAL::isa($target, 'HASH') ) {
    50          
    0          
133 6         5 my $key;
134 6         7 foreach $key (sort keys %{$target}) {
  6         25  
135 12         17 my $value = $target->{$key};
136 12 50       29 next unless (ref $value);
137 0 0       0 $DRefs{$value} = $CurrentDRef . $key unless ( exists $DRefs{$value} );
138 0         0 $Supress{$value} ++;
139             }
140 6 50       17 $result .= "{" if ($CurrentDepth);
141 6 50       16 $result .= "\n" if ($result);
142 6         7 $CurrentDepth ++;
143 6         7 foreach $key (sort keys %{$target}) {
  6         15  
144 12         75 $result .= ' ' x ( ($CurrentDepth - 1) * $Indent);
145 12         19 local $CurrentDRef = $CurrentDRef . $key;
146 12 50       23 $Supress{$target->{$key}} -- if ( ref $target->{$key} );
147 12         31 $result .= _astext($key) . ' = ' . _astext($target->{$key}) .";\n";
148             }
149 6         62 $CurrentDepth --;
150 6 50       20 $result .= ' 'x(($CurrentDepth-1) * $Indent) . "}" if ($CurrentDepth);
151 6         25 return $result;
152             }
153            
154             elsif ( UNIVERSAL::isa($target, 'ARRAY') ) {
155 8         11 my $key;
156             # If $MaxItems is set and there are fewer than that many non-ref items
157 8   66     23 my $one_line = ( $MaxItems and $#{$target} <= $MaxItems );
158 8         13 foreach $key (0 .. $#{$target}) {
  8         75  
159 524         631 my $value = $target->[$key];
160 524 100       1273 next unless (ref $value);
161 6         7 $one_line = 0;
162 6 50       22 $DRefs{$value} = $CurrentDRef . $key unless ( exists $DRefs{$value} );
163 6         13 $Supress{$value} ++;
164             }
165 8 100       24 my $joiner = ( $one_line ) ? " " : "\n";
166 8 50       21 $result .= "(" if ( $CurrentDepth );
167 8 50       20 $result .= $joiner if ( $result );
168 8         10 $CurrentDepth ++;
169 8         11 foreach $key (0 .. $#{$target}) {
  8         19  
170 524 100       9784 $result .= $one_line ? '' : ' ' x ( ($CurrentDepth - 1) * $Indent);
171 524         904 local $CurrentDRef = $CurrentDRef . $key;
172 524 100       1053 $Supress{$target->[$key]} -- if ( ref $target->[$key] );
173 524         956 $result .= _astext($target->[$key]) . "," . $joiner;
174             }
175 8         65 $CurrentDepth --;
176 8 0       28 $result .= ( ! $one_line ? ' 'x(($CurrentDepth-1) * $Indent) : '' ) . ")"
    50          
177             if ( $CurrentDepth );
178 8         404 return $result;
179             }
180            
181             elsif (UNIVERSAL::isa($target, 'REF') or UNIVERSAL::isa($target, 'SCALAR')) {
182 0         0 $result .= '/* REFERENCE */ ';
183 0         0 local $CurrentDepth = $CurrentDepth + 1;
184 0         0 local $CurrentDRef = $CurrentDRef . 0;
185 0         0 $result .= _astext($$target);
186 0         0 return $result;
187             }
188            
189             # Otherwise it's some unsupported kind of reference; just "" stringify it
190 0         0 return "/* REFERENCE TO $target */";
191             }
192              
193             ### Reader
194              
195 3     3   88 use vars qw( @TextLines $LineNumber $Source );
  3         6  
  3         3799  
196              
197             # $datastructure = fromtext($string);
198             # $datastructure = fromtext($string, %options);
199             # reconstruct an object graph from a NeXT property list.
200             sub fromtext ($%) {
201 16     16 1 1107 my $dictionary_text = shift;
202 16         44 my %options = @_;
203            
204 16         60 local @TextLines = split("\n", $dictionary_text);
205 16         23 local $LineNumber = 0;
206 16   100     72 local $Source = $options{'-source'} || '';
207            
208 16 100       54 if ( $options{'-scalar'} ) {
    50          
209 15         25 return _parse_value( _get_line() . "\000", "\000" );
210             } elsif ( $options{'-array'} ) {
211 1         5 return _parse_array();
212             } else {
213 0         0 return _parse_hash();
214             }
215             }
216              
217             # _parse_error( $message );
218             sub _parse_error {
219 0     0   0 my $message = shift;
220 0 0       0 warn 'PropertyList error, ' . $message .
221             ' at line ' . $LineNumber . ( $Source ? ' in ' . $Source : '' ) ."\n";
222             }
223              
224             # $text = _get_line;
225             sub _get_line {
226 27     27   29 $LineNumber++;
227 27         70 shift(@TextLines);
228             }
229              
230             # $hash_ref = _parse_hash();
231             sub _parse_hash {
232 3     3   5 my $hash = {};
233 3         3 my ($key, $value, $current_line);
234            
235 3         8 while (@TextLines) {
236 9         14 $current_line = _get_line();
237            
238             # Ignore comments
239 9         13 $current_line =~ s#\Q/*\E.*?\Q*/\E##g;
240            
241             # Ignore blank lines
242 9 50       23 next if ( $current_line =~ /^\s*$/ );
243            
244             # If we hit a closing brace, we're done with this hash
245 9 100       23 last if ( $current_line =~ /^\s*\}[,;]/o );
246            
247             # Extract key and equals sign.
248 6 50       36 if ( $current_line =~ s/^\s*\"(([^\"\\]|\\.)+)\"//o ) {
    50          
249 0         0 $key = unprintable( $1 );
250             } elsif ( $current_line =~ s/^\s*(\S+)//o ) {
251 6         14 $key = unprintable( $1 );
252             } else {
253 0         0 _parse_error("Key not found");
254 0         0 last;
255             }
256            
257 6 50       54 $current_line =~ s/^\s*=\s*//o or _parse_error("= not found");
258            
259             # Extract value
260 6         22 $value = _parse_value( $current_line, ';' );
261            
262 6 50       77 next unless (defined $key);
263            
264 6         18 $hash->{$key} = $value;
265             }
266            
267 3         8 return $hash;
268             }
269              
270             # $array_ref = _parse_array();
271             sub _parse_array {
272 1     1   18 my $array = [];
273 1         1 my ($value, $current_line);
274            
275 1         4 while (@TextLines) {
276 3         7 $current_line = _get_line();
277            
278             # Ignore comments
279 3         15 $current_line =~ s#\Q/*\E.*?\Q*/\E##g;
280            
281             # Ignore blank lines
282 3 50       12 next if ( $current_line =~ /^\s*$/ );
283            
284             # If we hit a closing paren, we're done with this hash
285 3 50       8 last if ( $current_line =~ /^\s*\)[,;]/o );
286            
287             # Extract value
288 3         7 $value = _parse_value( $current_line, ',' );
289            
290 3         10 push( @$array, $value);
291            
292 3         7 next;
293             }
294            
295 1         5 return $array;
296             }
297              
298             # $string = _parse_multiline($ender);
299             sub _parse_multiline {
300 0     0   0 my $ender = shift;
301            
302 0         0 my $value = '';
303 0         0 my $current_line;
304            
305 0         0 while (@TextLines) {
306 0         0 $current_line = _get_line();
307 0 0       0 last if ($current_line =~ /^\s*\Q$ender\E[\;\,]?\s*$/);
308 0         0 $value .= $current_line . "\n";
309             }
310 0         0 return $value;
311             }
312              
313             # $value = _parse_value( $value, $terminator );
314             # Extracts a quoted or unquoted string, an array, hash, or a multiline string
315             sub _parse_value {
316 24     24   32 my $current_line = shift;
317 24         29 my $end_value = shift;
318            
319 24 100       514 if ( $current_line =~ /^\s*\"(([^\"\\]|\\.)*)\"\Q$end_value\E\s*/ ) {
    100          
    50          
    50          
    0          
    0          
320             # Extract quoted value
321 10         29 return unprintable( $1 );
322             } elsif ( $current_line =~ /^\s*(\S+?)\Q$end_value\E\s*/ ) {
323             # Extract unquoted value
324 11         30 return unprintable( $1 );
325             } elsif ( $current_line =~ /^\s*(\/\*.*?\*\/)\s*\Q$end_value\E\s*/ ) {
326             # Extract comment
327 0         0 return undef;
328             } elsif ( $current_line =~ /^\s*\{/o ) {
329 3         7 return _parse_hash();
330             } elsif ( $current_line =~ /^\s*\(/o ) {
331 0           return _parse_array();
332             } elsif ( $current_line =~ /^\s*\<\<(\w+)(?:\Q$end_value\E)?/o ) {
333 0           return _parse_multiline($1);
334             } else {
335 0           _parse_error("value not found in '$current_line' - $end_value");
336             }
337             }
338              
339             1;
340              
341             __END__