blib/lib/HTML/ListToTree.pm | |||
---|---|---|---|
Criterion | Covered | Total | % |
statement | 183 | 202 | 90.5 |
branch | 57 | 94 | 60.6 |
condition | 39 | 62 | 62.9 |
subroutine | 29 | 30 | 96.6 |
pod | 19 | 20 | 95.0 |
total | 327 | 408 | 80.1 |
line | stmt | bran | cond | sub | pod | time | code |
---|---|---|---|---|---|---|---|
1 | =pod | ||||||
2 | |||||||
3 | =begin classdoc | ||||||
4 | |||||||
5 | Converts an HTML nested list document to a Javascripted | ||||||
6 | tree widget.
|
||||||
7 | |||||||
8 | Copyright© 2007, Dean Arnold, Presicient Corp., USA. All rights reserved.
|
||||||
9 | |||||||
10 | Excluding the dtree widget software and components included in the | ||||||
11 | L |
||||||
12 | under the same terms as Perl itself. Refer to the L |
||||||
13 | |||||||
14 | @author Dean Arnold | ||||||
15 | @since 2007-Jun-10 | ||||||
16 | @self $self | ||||||
17 | |||||||
18 | =end classdoc | ||||||
19 | |||||||
20 | =cut | ||||||
21 | |||||||
22 | package HTML::ListToTree; | ||||||
23 | |||||||
24 | 1 | 1 | 30986 | use HTML::TreeBuilder; | |||
1 | 45186 | ||||||
1 | 13 | ||||||
25 | 1 | 1 | 807 | use HTML::ListToTree::DTree; | |||
1 | 3 | ||||||
1 | 26 | ||||||
26 | |||||||
27 | 1 | 1 | 5 | use strict; | |||
1 | 2 | ||||||
1 | 26 | ||||||
28 | 1 | 1 | 5 | use warnings; | |||
1 | 1 | ||||||
1 | 3834 | ||||||
29 | |||||||
30 | our $VERSION = '0.10'; | ||||||
31 | |||||||
32 | our %tags_accepted = qw(a 1 li 1 ul 1 ol 1 /a 1 /li 1 /ul 1 /ol 1); | ||||||
33 | # | ||||||
34 | # have to use class variable for unlink action, due to | ||||||
35 | # recursive structure | ||||||
36 | # | ||||||
37 | our %unlinks = ( 'include' => 1, 'warn' => 1, 'ignore' => 1 ); | ||||||
38 | our $onUnlink; | ||||||
39 | |||||||
40 | =pod | ||||||
41 | |||||||
42 | =begin classdoc | ||||||
43 | |||||||
44 | Create an HTML::ListToTree object with specified text label and link url, | ||||||
45 | optionally setting an initial set of child nodes and/or extracting | ||||||
46 | children from a source document. | ||||||
47 | |||||||
48 | @constructor | ||||||
49 | @param Text a text label for the node | ||||||
50 | @param Link a link URL for the node. | ||||||
51 | @optional Children an arrayref of HTML::ListToTree objects | ||||||
52 | @optional Source a document from which to collect child nodes | ||||||
53 | @optional Widget either a Perl object, or the name of a Perl package, providing browser widget construction methods; | ||||||
54 | default 'HTML::ListToTree::DTree' | ||||||
55 | @optional UnlinkedLeaves string specifying disposition of unlinked leaf nodes; valid values are | ||||||
56 | |
||||||
57 | |
||||||
58 | |
||||||
59 | |
||||||
60 | |||||||
61 | |||||||
62 | @return an HTML::ListToTree object | ||||||
63 | |||||||
64 | =end classdoc | ||||||
65 | |||||||
66 | =cut | ||||||
67 | |||||||
68 | sub new { | ||||||
69 | 52 | 52 | 1 | 1318 | my $class = shift; | ||
70 | 52 | 135 | my %args = @_; | ||||
71 | |||||||
72 | 52 | 100 | 207 | $args{Children} ||= []; | |||
73 | 52 | 55 | my $widget; | ||||
74 | 52 | 100 | 101 | if ($args{Widget}) { | |||
75 | 1 | 50 | 3 | if (ref $args{Widget}) { | |||
76 | 0 | 0 | $widget = $args{Widget}; | ||||
77 | } | ||||||
78 | else { | ||||||
79 | 1 | 105 | eval " | ||||
80 | require $args{Widget}; | ||||||
81 | \$widget = $args{Widget}->new(); | ||||||
82 | "; | ||||||
83 | 1 | 50 | 6 | return undef if $@; | |||
84 | } | ||||||
85 | } | ||||||
86 | else { | ||||||
87 | 51 | 144 | $widget = HTML::ListToTree::DTree->new(); | ||||
88 | } | ||||||
89 | # | ||||||
90 | # if an unlink action is specified, update it | ||||||
91 | # | ||||||
92 | 52 | 0 | 122 | $onUnlink = $unlinks{lc $args{UnlinkedLeaves}} ? lc $args{UnlinkedLeaves} : 'include' | |||
50 | |||||||
93 | if exists $args{UnlinkedLeaves}; | ||||||
94 | |||||||
95 | 52 | 100 | 158 | $onUnlink ||= 'include'; | |||
96 | 52 | 270 | my $self = bless { | ||||
97 | _text => $args{Text}, | ||||||
98 | _link => $args{Link}, | ||||||
99 | _children => $args{Children}, | ||||||
100 | _widget => $widget, | ||||||
101 | }, $class; | ||||||
102 | |||||||
103 | 52 | 100 | 108 | push @{$self->{_children}}, $self->extractTree($args{Source}) | |||
3 | 13 | ||||||
104 | if exists $args{Source}; | ||||||
105 | |||||||
106 | 52 | 183 | return $self; | ||||
107 | } | ||||||
108 | |||||||
109 | =pod | ||||||
110 | |||||||
111 | =begin classdoc | ||||||
112 | |||||||
113 | Add a set of sibling nodes to the tree as a child of this node. | ||||||
114 | The nodes are appended to any existing list of immediate children | ||||||
115 | of this node. | ||||||
116 | |||||||
117 | @param @nodes a list of nodes. Nodes are specified as either 2-tuples of | ||||||
118 | Text => Link, or as an HTML::ListToTree object | ||||||
119 | |||||||
120 | @returnlist HTML::ListToTree objects added as children of this object | ||||||
121 | |||||||
122 | =end classdoc | ||||||
123 | |||||||
124 | =cut | ||||||
125 | |||||||
126 | sub addChildren { | ||||||
127 | 15 | 15 | 1 | 17 | my $self = shift; | ||
128 | 15 | 15 | my ($text, $link); | ||||
129 | 15 | 19 | my @nodes = (); | ||||
130 | 15 | 100 | 36 | my @args = (ref $_[0] eq 'ARRAY') ? @{$_[0]} : @_; | |||
14 | 29 | ||||||
131 | 15 | 100 | 42 | push(@{$self->{_children}}, | |||
38 | 180 | ||||||
132 | ref $args[0] ? shift @args : HTML::ListToTree->new(Text => shift @args, Link => shift @args)), | ||||||
133 | push(@nodes, $self->{_children}[-1]) | ||||||
134 | while (@args); | ||||||
135 | 15 | 53 | return @nodes; | ||||
136 | } | ||||||
137 | |||||||
138 | =pod | ||||||
139 | |||||||
140 | =begin classdoc | ||||||
141 | |||||||
142 | Extract a tree from a nested lists of the input document, and | ||||||
143 | add it as a child of this node. | ||||||
144 | |||||||
145 | @param $html the source HTML document | ||||||
146 | |||||||
147 | @returnlist HTML::ListToTree objects extracted from the document | ||||||
148 | |||||||
149 | =end classdoc | ||||||
150 | |||||||
151 | =cut | ||||||
152 | |||||||
153 | sub addFromDocument { | ||||||
154 | 1 | 1 | 1 | 2 | my ($self, $html) = @_; | ||
155 | 1 | 4 | my @nodes = $self->extractTree($html); | ||||
156 | 1 | 3 | push @{$self->{_children}}, @nodes; | ||||
1 | 3 | ||||||
157 | 1 | 6 | return @nodes; | ||||
158 | } | ||||||
159 | |||||||
160 | =pod | ||||||
161 | |||||||
162 | =begin classdoc | ||||||
163 | |||||||
164 | Return the child nodes of this node as a list. | ||||||
165 | The list is in the order in which the nodes were added | ||||||
166 | to this node. | ||||||
167 | |||||||
168 | @returnlist the child nodes aHTML::ListToTree objects | ||||||
169 | |||||||
170 | =end classdoc | ||||||
171 | |||||||
172 | =cut | ||||||
173 | |||||||
174 | sub getChildren { | ||||||
175 | 1 | 1 | 1 | 3 | my $self = shift; | ||
176 | |||||||
177 | 1 | 2 | return @{$self->{_children}}; | ||||
1 | 5 | ||||||
178 | } | ||||||
179 | |||||||
180 | =pod | ||||||
181 | |||||||
182 | =begin classdoc | ||||||
183 | |||||||
184 | Scans this node's children to locate a node with the specified text label. | ||||||
185 | The scan is breadth first (i.e., siblings are scanned before children). | ||||||
186 | |||||||
187 | @return if a match is found, an HTML::ListToTree object; otherwise, undef. | ||||||
188 | |||||||
189 | =end classdoc | ||||||
190 | |||||||
191 | =cut | ||||||
192 | |||||||
193 | sub getNodeByText { | ||||||
194 | 2 | 2 | 1 | 4 | my ($self, $text) = @_; | ||
195 | |||||||
196 | 2 | 2 | foreach (@{$self->{_children}}) { | ||||
2 | 5 | ||||||
197 | 6 | 100 | 17 | return $_ | |||
198 | if ($_->{_text} eq $text); | ||||||
199 | } | ||||||
200 | 1 | 2 | foreach (@{$self->{_children}}) { | ||||
1 | 3 | ||||||
201 | 1 | 5 | my $node = $_->getNodeByText($text); | ||||
202 | 1 | 50 | 7 | return $node if $node; | |||
203 | } | ||||||
204 | 0 | 0 | return undef; | ||||
205 | } | ||||||
206 | |||||||
207 | =pod | ||||||
208 | |||||||
209 | =begin classdoc | ||||||
210 | |||||||
211 | Scans this node's children to locate a node with the specified URL link. | ||||||
212 | The scan is breadth first (i.e., siblings are scanned before children). | ||||||
213 | |||||||
214 | @return if a match is found, an HTML::ListToTree object; otherwise, undef. | ||||||
215 | |||||||
216 | =end classdoc | ||||||
217 | |||||||
218 | =cut | ||||||
219 | |||||||
220 | sub getNodeByLink { | ||||||
221 | 17 | 17 | 1 | 19 | my ($self, $link) = @_; | ||
222 | |||||||
223 | 17 | 20 | my $offset = -1 * length($link); | ||||
224 | 17 | 15 | foreach (@{$self->{_children}}) { | ||||
17 | 29 | ||||||
225 | 23 | 100 | 57 | return $_ | |||
226 | if (substr($_->{_link}, $offset) eq $link); | ||||||
227 | } | ||||||
228 | 16 | 17 | foreach (@{$self->{_children}}) { | ||||
16 | 27 | ||||||
229 | 16 | 28 | my $node = $_->getNodeByLink($link); | ||||
230 | 16 | 100 | 36 | return $node if $node; | |||
231 | } | ||||||
232 | 14 | 18 | return undef; | ||||
233 | } | ||||||
234 | |||||||
235 | =pod | ||||||
236 | |||||||
237 | =begin classdoc | ||||||
238 | |||||||
239 | Return the text label of this node. | ||||||
240 | |||||||
241 | @return the text label of this node | ||||||
242 | |||||||
243 | =end classdoc | ||||||
244 | |||||||
245 | =cut | ||||||
246 | |||||||
247 | sub getText { | ||||||
248 | 15 | 15 | 1 | 88 | return $_[0]->{_text}; | ||
249 | } | ||||||
250 | |||||||
251 | =pod | ||||||
252 | |||||||
253 | =begin classdoc | ||||||
254 | |||||||
255 | Set the text label of this node. | ||||||
256 | |||||||
257 | @param $text the text label to set | ||||||
258 | |||||||
259 | @return this node | ||||||
260 | |||||||
261 | =end classdoc | ||||||
262 | |||||||
263 | =cut | ||||||
264 | |||||||
265 | sub setText { | ||||||
266 | 1 | 1 | 1 | 3 | $_[0]->{_text} = $_[1]; | ||
267 | 1 | 3 | return $_[0]; | ||||
268 | } | ||||||
269 | |||||||
270 | =pod | ||||||
271 | |||||||
272 | =begin classdoc | ||||||
273 | |||||||
274 | Return the link URL of this node. | ||||||
275 | |||||||
276 | @return the link URL of this node | ||||||
277 | |||||||
278 | =end classdoc | ||||||
279 | |||||||
280 | =cut | ||||||
281 | |||||||
282 | sub getLink { | ||||||
283 | 15 | 15 | 1 | 106 | return $_[0]->{_link}; | ||
284 | } | ||||||
285 | |||||||
286 | =pod | ||||||
287 | |||||||
288 | =begin classdoc | ||||||
289 | |||||||
290 | Set the link URL of this node. | ||||||
291 | |||||||
292 | @param $link the link URL to set | ||||||
293 | |||||||
294 | @return this node | ||||||
295 | |||||||
296 | =end classdoc | ||||||
297 | |||||||
298 | =cut | ||||||
299 | |||||||
300 | sub setLink { | ||||||
301 | 1 | 1 | 1 | 4 | $_[0]->{_link} = $_[1]; | ||
302 | 1 | 3 | return $_[0]; | ||||
303 | } | ||||||
304 | |||||||
305 | =pod | ||||||
306 | |||||||
307 | =begin classdoc | ||||||
308 | |||||||
309 | Render this HTML::ListToTree object into an HTML document containing Javascript | ||||||
310 | required for dtree, and suitable for use as a frame | ||||||
311 | within a frameset. Subclasses may override this method to provide | ||||||
312 | alternate renderings of the tree. | ||||||
313 | |||||||
314 | @constructor | ||||||
315 | @optional Additions HTML text to be appended to the generated tree | ||||||
316 | @optional BasePath the base directory path for all local hyperlinks | ||||||
317 | @optional CloseIcon name of icon used for closed tree nodes; default 'closedbook.gif' | ||||||
318 | @optional CSSPath path to the stylesheet file dtree.css used by dtree; default './css' | ||||||
319 | @optional IconPath path to the location of icons used by dtree; default './img' | ||||||
320 | @optional JSPath path to the Javascript file dtree.js; default '.js' | ||||||
321 | @optional UseIcons when set to a true value, tree nodes are decorated with icons; default true | ||||||
322 | @optional OpenIcon name of icon used for open tree nodes; default 'openbook.gif' | ||||||
323 | @optional RootIcon name of icon used for the root tree node; default is same as OpenIcon | ||||||
324 | @optional Target the name of an HTML frame to contain the document being navigated; default 'mainframe' | ||||||
325 | |||||||
326 | @return an HTML document | ||||||
327 | |||||||
328 | =end classdoc | ||||||
329 | |||||||
330 | =cut | ||||||
331 | |||||||
332 | sub render { | ||||||
333 | 3 | 3 | 1 | 992 | my $self = shift; | ||
334 | 3 | 16 | my %args = @_; | ||||
335 | |||||||
336 | 3 | 100 | 12 | $args{CloseIcon} ||= 'closedbook.gif'; | |||
337 | 3 | 100 | 14 | $args{OpenIcon} ||= 'openbook.gif'; | |||
338 | 3 | 100 | 10 | $args{IconPath} ||= './img'; | |||
339 | 3 | 100 | 8 | $args{CSSPath} ||= './css/dtree.css'; | |||
340 | 3 | 100 | 11 | $args{JSPath} ||= './js/dtree.js'; | |||
341 | 3 | 66 | 8 | $args{RootIcon} ||= $args{OpenIcon}; | |||
342 | 3 | 100 | 9 | $args{Target} ||= 'mainframe'; | |||
343 | 3 | 100 | 9 | $args{Additions} ||= ''; | |||
344 | |||||||
345 | 3 | 100 | 10 | $args{UseIcons} = 1 unless exists $args{UseIcons}; | |||
346 | |||||||
347 | 3 | 100 | 16 | my ($openimg, $closeimg, $rootimg) = $args{UseIcons} ? | |||
348 | ("$args{IconPath}/$args{OpenIcon}", | ||||||
349 | "$args{IconPath}/$args{CloseIcon}", | ||||||
350 | "$args{IconPath}/$args{RootIcon}") : | ||||||
351 | ('', '', ''); | ||||||
352 | # | ||||||
353 | # adjust paths for css/javascript/images | ||||||
354 | # | ||||||
355 | 3 | 50 | 9 | if ($args{BasePath}) { | |||
356 | $args{$_} = _pathAdjust($args{BasePath}, $args{$_}) | ||||||
357 | 0 | 0 | foreach (qw(JSPath CSSPath IconPath)); | ||||
358 | 0 | 0 | 0 | $self->{_link} = _pathAdjust($args{BasePath}, $self->{_link}) | |||
359 | if $self->{_link}; | ||||||
360 | } | ||||||
361 | # | ||||||
362 | # save path info if needed later | ||||||
363 | # | ||||||
364 | 3 | 7 | $self->{_jspath} = $args{JSPath}; | ||||
365 | 3 | 4 | $self->{_iconpath} = $args{IconPath}; | ||||
366 | 3 | 5 | $self->{_csspath} = $args{CSSPath}; | ||||
367 | 3 | 100 | 32 | $self->{_widget}->start( | |||
368 | IconPath => $args{IconPath}, | ||||||
369 | CSSPath => $args{CSSPath}, | ||||||
370 | JSPath => $args{JSPath}, | ||||||
371 | UseIcons => $args{UseIcons} || 0, | ||||||
372 | RootIcon => $rootimg, | ||||||
373 | RootText => $self->{_text}, | ||||||
374 | RootLink => $self->{_link}, | ||||||
375 | Target => $args{Target}, | ||||||
376 | OpenIcon => $openimg, | ||||||
377 | CloseIcon => $closeimg, | ||||||
378 | ); | ||||||
379 | # | ||||||
380 | # sort current tree into levels | ||||||
381 | # | ||||||
382 | 3 | 8 | my @levels = ( [ $self ] ); | ||||
383 | 3 | 8 | _sort_tree([ $self ], \@levels); | ||||
384 | # | ||||||
385 | # draw root level first | ||||||
386 | # | ||||||
387 | 3 | 8 | my ($close, $open); | ||||
388 | 3 | 4 | shift @levels; | ||||
389 | 3 | 6 | foreach (@{$self->{_children}}) { | ||||
3 | 8 | ||||||
390 | 15 | 65 | $_->{_text}=~s/'/\\'/g; | ||||
391 | 15 | 50 | 29 | $_->{_link} = _pathAdjust($args{BasePath}, $_->{_link}) | |||
392 | if $args{BasePath}; | ||||||
393 | 15 | 100 | 100 | 16 | (($#{$_->{_children}} >= 0) && $args{UseIcons}) ? | ||
394 | $self->{_widget}->add($_->{_node}, 0, $_->{_text}, $_->{_link}) : | ||||||
395 | $self->{_widget}->addLeaf($_->{_node}, 0, $_->{_text}, $_->{_link}); | ||||||
396 | } | ||||||
397 | # | ||||||
398 | # then draw succeding levels | ||||||
399 | # | ||||||
400 | 3 | 4 | my $offset = scalar @{$levels[0]}; | ||||
3 | 7 | ||||||
401 | 3 | 6 | foreach my $i (1..$#levels) { | ||||
402 | 9 | 9 | foreach (@{$levels[$i]}) { | ||||
9 | 21 | ||||||
403 | 102 | 125 | $_->{_node} += $offset; | ||||
404 | 102 | 140 | $_->{_text}=~s/'/\\'/g; | ||||
405 | 102 | 50 | 176 | $_->{_link} = _pathAdjust($args{BasePath}, $_->{_link}) | |||
406 | if $args{BasePath}; | ||||||
407 | 102 | 100 | 100 | 87 | (($#{$_->{_children}} >= 0) && $args{UseIcons}) ? | ||
408 | $self->{_widget}->add($_->{_node}, $levels[$i-1][$_->{_parent}]->{_node}, $_->{_text}, $_->{_link}) : | ||||||
409 | $self->{_widget}->addLeaf($_->{_node}, $levels[$i-1][$_->{_parent}]->{_node}, $_->{_text}, $_->{_link}); | ||||||
410 | } | ||||||
411 | 9 | 13 | $offset += scalar @{$levels[$i]}; | ||||
9 | 23 | ||||||
412 | } | ||||||
413 | |||||||
414 | 3 | 11 | return $self->{_widget}->getWidget($args{Additions}); | ||||
415 | } | ||||||
416 | |||||||
417 | sub _pathAdjust { | ||||||
418 | 0 | 0 | 0 | my ($path, $jspath) = @_; | |||
419 | 0 | 0 | 0 | 0 | return $jspath | ||
420 | unless (substr($jspath, 0, 2) eq './') && (substr($path, 0, 2) eq './'); | ||||||
421 | # | ||||||
422 | # relative path, adjust as needed from current base | ||||||
423 | # | ||||||
424 | 0 | 0 | my @parts = split /\//, $path; | ||||
425 | 0 | 0 | my @jsparts = split /\//, $jspath; | ||||
426 | 0 | 0 | shift @parts; | ||||
427 | 0 | 0 | shift @jsparts; # and the relative lead | ||||
428 | 0 | 0 | my $prefix = ''; | ||||
429 | 0 | 0 | 0 | shift @parts, | |||
0 | |||||||
430 | shift @jsparts | ||||||
431 | while @parts && @jsparts && ($parts[0] eq $jsparts[0]); | ||||||
432 | 0 | 0 | return ('../' x scalar @parts) . join('/', @jsparts) | ||||
433 | } | ||||||
434 | |||||||
435 | =pod | ||||||
436 | |||||||
437 | =begin classdoc | ||||||
438 | |||||||
439 | Extract the nested list from the supplied HTML document and convert it | ||||||
440 | to an HTML::ListToTree object. Subclasses may override this method | ||||||
441 | to provide alternate list extraction logic. | ||||||
442 | |||||||
443 | @param $html the source document | ||||||
444 | |||||||
445 | @return an HTML::ListToTree object | ||||||
446 | |||||||
447 | =end classdoc | ||||||
448 | |||||||
449 | =cut | ||||||
450 | |||||||
451 | sub extractTree { | ||||||
452 | 4 | 4 | 1 | 6 | my ($self, $src) = @_; | ||
453 | # | ||||||
454 | # enforce some canonical form: only start with a list, | ||||||
455 | # remove all comments, and insert list items between | ||||||
456 | # consecutive list elements | ||||||
457 | # | ||||||
458 | 4 | 14 | $src=~s/ |