File Coverage

blib/lib/Data/Foswiki.pm
Criterion Covered Total %
statement 96 116 82.7
branch 48 62 77.4
condition 19 30 63.3
subroutine 9 11 81.8
pod 2 2 100.0
total 174 221 78.7


line stmt bran cond sub pod time code
1             package Data::Foswiki;
2              
3 2     2   41155 use 5.006;
  2         7  
  2         71  
4 2     2   11 use strict;
  2         4  
  2         66  
5 2     2   12 use warnings;
  2         8  
  2         68  
6              
7 2     2   9 use Exporter 'import';
  2         4  
  2         3177  
8             our @EXPORT_OK = qw(serialise deserialise);
9              
10             =head1 NAME
11              
12             Data::Foswiki - Read and Write Foswiki topics
13              
14             =head1 VERSION
15              
16             Version 0.02
17              
18             =cut
19              
20             our $VERSION = '0.03';
21              
22             =head1 SYNOPSIS
23              
24             Quickly read and write Foswiki topics into a hash
25              
26             use Data::Foswiki;
27              
28             #read
29             my $fh;
30             open($fh, '<', '/var/lib/foswiki/data/System/FAQSimultaneousEdits.txt') or die 'open failure';
31             my @topic_text = <$fh>;
32             close($fh);
33             my $topic = Data::Foswiki::Test2::deserialise(@topic_text);
34            
35             $topic->{TOPICINFO}{author} = 'NewUser';
36             $topic->{PARENT}{name} = 'WebHome';
37            
38             $topic->{TEXT} = "Some new text\n\n".$topic->{TEXT};
39             undef $topic->{TOPICMOVED};
40            
41             $topic->{FIELD}{TopicTitle}{attributes} = 'H';
42            
43             #add a new field that is not part of the form definition - if edited within foswiki, it willbe removed
44             #but its useful for importing
45             $topic->{FIELD}{NewField}{value} = 'test';
46            
47             #write
48             open($fh, '>', '/var/lib/foswiki/data/System/FAQNewFaq.txt') or die 'write failure';
49             print $fh Data::Foswiki::Test::serialise($topic);
50             close($fh);
51            
52             =head1 SUBROUTINES/METHODS
53              
54             =head2 deserialise($text|@stringarray) -> $hash_ref
55              
56             Parse a string, or array of strings and convert into a hash of the Foswiki topic's data
57              
58             (apparently Perl can be faster reading a file into an array)
59              
60             if you pass in an undef / empty string, you will get undef back
61              
62             =cut
63              
64             our $isValidEmbedding_func;
65              
66             my $METAINFOregex = qr/^\%META:(TOPICINFO){(.*)}\%\n?$/o;
67             my $METAPARENTregex = qr/^\%META:(TOPICPARENT){(.*)}\%\n?$/o;
68             my $METAregex = qr/^\%META:(\S*){(.*)}\%\n?$/o;
69              
70             sub deserialise {
71             # use Data::Dumper;
72             # die 'deserialise'.Dumper($_[0]) if (ref(\$_[0]) ne 'STRING');
73 7     7 1 2251 my $topic;
74              
75 7 100       27 return $topic unless ( $#_ >= 0 );
76              
77             #convert a string into an array
78 5 100       18 if ( $#_ == 0 ) {
79 2 50       12 return $topic if ( $_[0] eq '' );
80 2 100       10 if ( $_[0] =~ /\n/ ) {
81 1         6 my @lines = split( /\n/, $_[0] );
82             #split will not give you an empty trailing array element if \n is the last char in the string
83             #TODO: do i really need to make a copy of the array?
84 1 50       6 push(@lines, '') if ($_[0] =~ m/\n$/);
85 1         9 return deserialise( @lines );
86             }
87             }
88              
89 4         7 my $start = 0;
90 4         7 my $end = -1;
91              
92             #I can test $_[$start] rather than defined($_[$start])
93             # because an empty line still would not match the regex
94             # first get rid of the leading META
95 4 100 66     46 if ( $_[$start] && $_[$start] =~ $METAINFOregex ) {
96 2         8 my $hash = _readKeyValues($2);
97 2 50 33     11 if (!$isValidEmbedding_func || &$isValidEmbedding_func(undef, $1, $hash)) {
98 2         9 $topic->{$1} = $hash;
99 2         5 $start++;
100             }
101             }
102              
103             #turns out that the trailing newline removeal code in LegacyMeta is terrible
104             # it removes a trailing newline even when there is a TOPICPARENT, and when there are rejected META's too
105 4         5 my $trailingMeta;
106              
107 4 100 100     35 if ( $_[$start] && $_[$start] =~ $METAPARENTregex ) {
108 1         4 my $hash = _readKeyValues($2);
109 1         3 $trailingMeta++;
110 1 50 33     6 if (!$isValidEmbedding_func || &$isValidEmbedding_func(undef, $1, $hash)) {
111 1         4 $topic->{$1} = $hash;
112 1         3 $start++;
113             }
114             }
115              
116             #then the trailing META
117 4   66     42 while ( $_[$end] && $_[$end] =~ $METAregex ) {
118             #LegacyMeta compatibility hack :/
119 8         11 $trailingMeta++;
120              
121             #should skip any TOPICINFO & TOPICPARENT, they are _only_ valid in one place in the file.
122 8 100 66     50 last if ( ( $1 eq 'TOPICINFO' ) || ( $1 eq 'TOPICPARENT' ) );
123              
124 7         41 my $meta = _readKeyValues($2);
125 7 50 33     21 if ($isValidEmbedding_func && ! &$isValidEmbedding_func(undef, $1, $meta)) {
126 0         0 last;
127             }
128            
129             #I had hoped that we only removed the newlines if there was valid trailing meta... but no
130 7         10 $trailingMeta++;
131 7         8 $end--;
132              
133 7 100       57 if ( $1 eq 'FORM' ) {
134 1         9 $topic->{$1} = $meta;
135             }
136             else {
137 6 100 66     33 if ( exists( $meta->{name} ) && $1 ne 'FORM' ) {
138 5         75 $topic->{$1}{ $meta->{name} } = $meta;
139             }
140             else {
141 1         13 $topic->{$1} = $meta;
142             }
143             }
144             }
145              
146             #there is an extra newline added between TEXT and any trailing meta
147 4 100 100     24 $end-- if ( $trailingMeta && $_[$end] =~ /^\n?$/o );
148             #$end-- if ( $_[$end] =~ /^\n?$/o );
149              
150 4 100       14 if ( defined($_[$start]) ) {
151              
152             #TODO: not joining and just returning an arrayref is very much faster
153             #but leaves the user to work out if there are \n's
154             #perhaps this is a reson to wrap it in a class and provide a text() :/
155 3 100       27 $topic->{TEXT} =
156             join( ( ( $_[$start] =~ /\n/o ) ? '' : "\n" ), @_[ $start .. $#_ + $end + 1 ] );
157             #I'm not 100% sure about this, but if there's no trailing META, the unit tests suggest we need to add a \n
158             #if ($end == -1) {
159             # $topic->{TEXT} .= "\n";
160             #}
161             #OMG THIS IS SO CRAP.
162 3 50 66     30 if (!$trailingMeta && $topic->{TEXT} =~ /^(%META:([^{]+){(.*)}%\n)/) {
163 0         0 $topic->{TEXT} =~ s/\n$//s;
164             }
165             }
166 4         17 return $topic;
167             }
168              
169             =head2 serialise($hashref) -> string
170              
171             Serialise into a foswiki 'embedded' formatted string, ready for writing to disk.
172              
173             Note: this does not take care of updating the topic revision and date data
174              
175             =cut
176              
177             sub serialise {
178 1     1 1 1359 my $topic = shift;
179 1         5 my @ordered_keys = qw/TOPICINFO TOPICPARENT TEXT FORM TOPICMOVED FIELD/;
180 1         12 my @topic_keys = keys(%$topic);
181             #use Data::Dumper;
182             #print STDERR ">>>+>>>>>".Dumper($topic)."<<<<<<<\n";
183              
184             #I thought there was an extra \n added..
185 1         4 my $key_count = $#topic_keys;
186 1         482 my @text;
187              
188             my %done;
189 1         4 foreach my $type ( @ordered_keys, @topic_keys ) {
190 7 100       17 last if ($key_count < 0);
191 6 50       17 if ( !$done{$type} ) {
192 6         10 $done{$type} = 1;
193 6 50       15 next unless (exists($topic->{$type}));
194             #use Data::Dumper;
195             #print STDERR ">>>>>>>>".Dumper($topic->{$type})."<<<<<<<\n";
196              
197 6         7 $key_count--;
198 6 100       11 if ( $type eq 'TEXT' ) {
199             #print STDERR "TEXT == ".ref($topic->{TEXT})."\n";
200 1         3 push( @text, $topic->{TEXT} );
201 1 50       5 push( @text, '') if ( $key_count >= 0 );
202             }
203             else {
204 5 50       6 next unless (keys(%{$topic->{$type}}));
  5         19  
205 5         13 push( @text, _writeMeta( $type, $topic->{$type} ) );
206             }
207             }
208             }
209              
210             #TODO: how about using wantarray to avoid the join?
211 1         11 return join( "\n", @text );
212             }
213              
214             #from Foswiki::Meta
215             # STATIC Build a hash by parsing name=value comma separated pairs
216             # SMELL: duplication of Foswiki::Attrs, using a different
217             # system of escapes :-(
218             sub _readKeyValues {
219 10     10   120 my @arr = split( /="([^"]*)"\s*/, $_[0] );
220              
221             #if the last attribute is an empty string, we're a bit naf
222 10         27 my $count = $#arr;
223 10 100       38 push( @arr, '' ) unless ( $count % 2 );
224 10         16 my $res;
225 10         31 for ( my $i = 1 ; $i <= $count ; $i = $i + 2 ) {
226 33         55 $arr[$i] =~ s/%([\da-f]{2})/chr(hex($1))/geio;
  0         0  
227 33         137 $res->{ $arr[ $i - 1 ] } = $arr[$i];
228             }
229              
230 10         32 return $res;
231             }
232              
233             sub _writeMeta {
234 10     10   13 my $type = shift;
235 10         11 my $hash = shift;
236 10         13 my $string = '';
237              
238 10         35 while(my ($k, $v) = each(%$hash)) {
239 14 100       32 if (ref($v) eq 'HASH') {
240 5 100       11 $string .= "\n" if ($string ne '');
241 5         13 $string .= _writeMeta($type, $v);
242             } else {
243             #not a multi-value META (ie, TOPICINFO, TOPICPARENT, FORM)
244 9         16 last;
245             }
246             }
247            
248 10 100       25 if ($string eq '') {
249 9         19 $string .= '%META:' . $type . '{';
250 9 100       32 $string .= 'name="'.$hash->{name}.'" ' if (defined($hash->{name}));
251 9         23 foreach (keys %$hash) {
252 29 100       69 next if ($_ eq 'name');
253 22         45 $string .= $_.'="'._dataEncode( $hash->{$_} ).'" ';
254             }
255             #chop($string);
256            
257 9         17 $string.= '}%';
258             }
259             #use Data::Dumper;
260             #print STDERR ":::::".scalar(keys(%$hash))."::".Dumper($hash)."\n$string\n";
261 10         45 return $string;
262             ##################old code
263              
264 0         0 my @elements = _writeKeyValues( $type, $hash );
265             #use Data::Dumper;
266             #die "=====$type --".Dumper($hash)."--".Dumper(\@elements)."--\n" if (!defined($elements[0]));
267 0 0       0 unless ( $elements[0] =~ /^%META/ ) {
268 0         0 return '%META:' . $type . '{' . join( ' ', @elements ) . '}%';
269             }
270 0         0 return @elements;
271             }
272              
273             sub _writeKeyValues {
274 0     0   0 my $type = shift;
275 0         0 my $hash = shift;
276              
277 0         0 my $name;
278             return map {
279              
280 0 0       0 if ( ref( $hash->{$_} ) eq 'HASH' ) {
  0         0  
281              
282             #META:TYPE{name=} hash of entries
283 0         0 _writeMeta( $type, $hash->{$_} );
284             }
285             else {
286 0         0 $_ . '="' . _dataEncode( $hash->{$_} ) . '"';
287             }
288 0         0 } keys( %{$hash} );
289             }
290              
291             sub _dataDecode {
292 0     0   0 my $datum = shift;
293              
294 0         0 $datum =~ s/%([\da-f]{2})/chr(hex($1))/gei;
  0         0  
295 0         0 return $datum;
296             }
297              
298             sub _dataEncode {
299 22     22   29 my $datum = shift;
300              
301 22         33 $datum =~ s/([%"\r\n{}])/'%'.sprintf('%02x',ord($1))/ge;
  0         0  
302 22         73 return $datum;
303             }
304              
305             =head1 AUTHOR
306              
307             Sven Dowideit, C<< >>
308              
309             =head1 BUGS
310              
311             Please report any bugs or feature requests to C, or through
312             the web interface at L. I will be notified, and then you'll
313             automatically be notified of progress on your bug as I make changes.
314              
315              
316              
317              
318             =head1 SUPPORT
319              
320             Foswiki support can be found in the #foswiki irc channel on L,
321             or from SvenDowideit L
322              
323              
324             =head1 ACKNOWLEDGEMENTS
325              
326             =head1 TO DO
327              
328             make an XS version, and try a few different approaches to parsing and then benchmark them
329             this would mean making this module into a facade to the other implementations.
330              
331             is it faster not to modify the array? (just keep start and end Text indexes?)
332              
333             =head1 LICENSE AND COPYRIGHT
334              
335             Copyright 2012 Sven Dowideit SvenDowideit@fosiki.com.
336              
337             This program is free software; you can redistribute it and/or modify it
338             under the terms of either: the GNU General Public License as published
339             by the Free Software Foundation; or the Artistic License.
340              
341             See http://dev.perl.org/licenses/ for more information.
342              
343              
344             =cut
345              
346             1; # End of Data::Foswiki::Test