File Coverage

blib/lib/PPI/HTML/CodeFolder.pm
Criterion Covered Total %
statement 233 260 89.6
branch 67 118 56.7
condition 15 41 36.5
subroutine 21 23 91.3
pod 11 11 100.0
total 347 453 76.6


line stmt bran cond sub pod time code
1             =pod
2            
3             =begin classdoc
4            
5             Subclasses <cpan>PPI::HTML</cpan> to add code folding for POD,
6             comments, and 'use'/'require' statements. Optionally permits abbreviation
7             of standard PPI::HTML class/token names with user specified
8             replacements. For line number output, moves the line numbers
9             from individual &lt;span&gt;'s to a single table column,
10             with the source body in the 2nd column.
11             <p>
12             Copyright&copy; 2007, Presicient Corp., USA
13             All rights reserved.
14             <p>
15             Permission is granted to use this software under the terms of the
16             <a href='http://perldoc.perl.org/perlartistic.html'>Perl Artisitic License</a>.
17            
18             @author D. Arnold
19             @since 2007-01-22
20             @self $self
21            
22            
23             =end classdoc
24            
25             =cut
26              
27              
28             package PPI::HTML::CodeFolder;
29              
30 2     2   32672 use PPI::HTML;
  2         481232  
  2         96  
31 2     2   26 use base ('PPI::HTML');
  2         5  
  2         287  
32              
33 2     2   30 use strict;
  2         5  
  2         77  
34 2     2   13 use warnings;
  2         3  
  2         14559  
35              
36             our $VERSION = '1.01';
37              
38             our %classabvs = qw(
39            
40             arrayindex ai
41             backtick bt
42             cast cs
43             comment ct
44             core co
45             data dt
46             double db
47             end en
48             heredoc hd
49             heredoc_content hc
50             heredoc_terminator ht
51             interpolate ip
52             keyword kw
53             label lb
54             line_number ln
55             literal ll
56             magic mg
57             match mt
58             number nm
59             operator op
60             pod pd
61             pragma pg
62             prototype pt
63             readline rl
64             regex re
65             regexp re
66             separator sp
67             single sg
68             structure st
69             substitute su
70             symbol sy
71             transliterate tl
72             word wo
73             words wd
74            
75             );
76             #
77             # fold section regular expressions
78             #
79             my %foldres = (
80             Whitespace => [
81             qr/\G(?<=<pre>)((?:\s*<br>)+)/,
82             qr/\G.*?<br>((?:\s*<br>)+)/
83             ],
84             Comments => [
85             qr/\G(?<=<pre>)\s*(<span\s+class=['"]comment['"]>.+?<\/span>)(?=<br>)/,
86             qr/\G.*?<br>\s*(<span\s+class=['"]comment['"]>.+?<\/span>)(?=<br>)/
87             ],
88             POD => [
89             qr/\G(?<=<pre>)\s*(<span\s+class=['"]pod['"]>.+?<\/span>)(?=<br>)/,
90             qr/\G.*?<br>\s*(<span\s+class=['"]pod['"]>.+?<\/span>)(?=<br>)/
91             ],
92             Heredocs => [
93             qr/\G(?<=<pre>)\s*(<span\s+class=['"]heredoc_content['"]>.+?<\/span>)(?=<br>)/,
94             qr/\G.*?<br>\s*(<span\s+class=['"]heredoc_content['"]>.+?<\/span>)(?=<br>)/
95             ],
96             Imports => [
97             qr/\G(?<=<pre>)\s*
98             (
99             (?:<span\s+class=['"]keyword['"]>(?:use|require)<\/span>.+?;\s*)+
100             (?:<span\s+class=['"]comment['"]>.+?<\/span>)?
101             )
102             (?=<br>)
103             /x,
104             qr/\G.*?<br>\s*
105             (
106             (?:<span\s+class=['"]keyword['"]>(?:use|require)<\/span>.+?;\s*)+
107             (?:<span\s+class=['"]comment['"]>.+?<\/span>)?
108             )
109             (?=<br>)
110             /x
111             ],
112             );
113              
114             #
115             # folddiv CSS
116             #
117             our $ftcss = <<'EOFTCSS';
118            
119             .popupdiv {
120             font-family: fixed, Courier;
121             font-size: 8pt;
122             font-style: normal;
123             /* lineheight: 10pt; */
124             border:solid 1px #666666;
125             padding:4px;
126             position:absolute;
127             z-index:100;
128             visibility: hidden;
129             color: black;
130             top:10px;
131             left:20px;
132             width:auto;
133             height:auto;
134             background-color:#ffffcc;
135             layer-background-color:#ffffcc;
136             /* opacity: .9;
137             filter: alpha(opacity=90); */
138             overflow : hidden; // to keep FF on OS X happy
139             }
140            
141             .folddiv {
142             position: absolute;
143             visibility: hidden;
144             overflow : hidden; // to keep FF on OS X happy
145             }
146            
147             .bodypre {
148             font-family: fixed, Courier;
149             font-size: 9pt;
150             line-height: 13pt;
151             text-align: left;
152             color: black;
153             }
154            
155             .lnpre {
156             font-family: fixed, Courier;
157             font-size: 9pt;
158             line-height: 13pt;
159             text-align: right;
160             color: #666666;
161             }
162            
163             .foldfill {
164             font-family: fixed, Courier;
165             font-size: 8pt;
166             font-style: italic;
167             line-height: 13pt;
168             color: blue;
169             }
170            
171             .foldbtn {
172             font-family: fixed, Courier;
173             font-size: 9pt;
174             line-height: 13pt;
175             color: blue;
176             }
177            
178             -->
179             </style>
180             EOFTCSS
181             #
182             # fold expansion javascript
183             #
184             our $ftjs = <<'EOFTJS';
185            
186             function ppiHtmlCF(startlines, endlines)
187             {
188             this.startlines = startlines;
189             this.endlines = endlines;
190             this.foldstate = [];
191             for (var i = 0; i < startlines.length; i++)
192             this.foldstate[startlines[i]] = "closed";
193             }
194            
195             // Clears the cookie
196             ppiHtmlCF.prototype.clearCookie = function()
197             {
198             var now = new Date();
199             var yesterday = new Date(now.getTime() - 1000 * 60 * 60 * 24);
200             this.setCookie('', yesterday);
201             };
202            
203             // Sets value in the cookie
204             ppiHtmlCF.prototype.setCookie = function(cookieValue, expires)
205             {
206             document.cookie =
207             'ppihtmlcf=' + escape(cookieValue) + ' ' +
208             + (expires ? '; expires=' + expires.toGMTString() : '')
209             + ' path=' + location.pathname;
210             };
211            
212             // Gets the cookie
213             ppiHtmlCF.prototype.getCookie = function() {
214             var cookieValue = '';
215             var posName = document.cookie.indexOf('ppihtmlcf=');
216             if (posName != -1) {
217             var posValue = posName + 'ppihtmlcf='.length;
218             var endPos = document.cookie.indexOf(';', posValue);
219             if (endPos != -1) cookieValue = unescape(document.cookie.substring(posValue, endPos));
220             else cookieValue = unescape(document.cookie.substring(posValue));
221             }
222             return (cookieValue);
223             };
224            
225             // updates cookie with current set of unfolded section startlines as a string
226             ppiHtmlCF.prototype.updateCookie = function()
227             {
228             var str = ':';
229             for (var n = 0; n < this.startlines.length; n++) {
230             line = this.startlines[n];
231             if ((this.foldstate[line] != null) && (this.foldstate[line] == "open"))
232             str += line + ':';
233             }
234             if (str == ':')
235             this.setCookie('');
236             else
237             this.setCookie(str);
238             };
239            
240             // forces all folds to current cookie state
241             ppiHtmlCF.prototype.openFromCookie = function()
242             {
243             var opened = this.getCookie();
244             if (opened == '') {
245             /*
246             * no cookie, create one w/ all folds closed
247             */
248             this.setCookie('');
249             }
250             else {
251             for (var n = 0; n < this.startlines.length; n++) {
252             line = this.startlines[n];
253             if (this.foldstate[line] == null)
254             this.foldstate[line] = "closed";
255            
256             if (opened.indexOf(':' + line + ':') >= 0) {
257             if (this.foldstate[line] == "closed")
258             this.accordian(line, this.endlines[n]);
259             }
260             else {
261             if (this.foldstate[line] == "open")
262             this.accordian(line, this.endlines[n]);
263             }
264             }
265             }
266             };
267            
268             /*
269             * renders line number and fold button margins
270             */
271             ppiHtmlCF.prototype.renderMargins = function(lastline)
272             {
273             var start = 1;
274             var lnmargin = '';
275             var btnmargin = '';
276             for (var i = 0; i < this.startlines.length; i++) {
277             if (start != this.startlines[i]) {
278             for (var j = start; j < this.startlines[i]; j++) {
279             lnmargin += j + "\n";
280             btnmargin += "\n";
281             }
282             }
283             start = this.endlines[i] + 1;
284             lnmargin += "<span id='lm" + this.startlines[i] + "' class='lnpre'>" + this.startlines[i] + "</span>\n";
285             btnmargin += "<a id='ll" + this.startlines[i] + "' class='foldbtn' " +
286             "onclick=\"ppihtml.accordian(" + this.startlines[i] + "," + this.endlines[i] + ")\">&oplus;</a>\n";
287             }
288             if (lastline > this.endlines[this.endlines.length - 1]) {
289             for (var j = start; j <= lastline; j++) {
290             lnmargin += j + "\n";
291             btnmargin += "\n";
292             }
293             }
294             lnmargin += "\n";
295             btnmargin += "\n";
296             buttons = document.getElementById("btnmargin");
297             lnno = document.getElementById("lnnomargin");
298             if (navigator.appVersion.indexOf("MSIE")!=-1) {
299             lnno.outerHTML = "<pre id='lnnomargin' class='lnpre'>" + lnmargin + "</pre>";
300             buttons.outerHTML = "<pre id='btnmargin' class='lnpre'>" + btnmargin + "</pre>";
301             }
302             else {
303             lnno.innerHTML = lnmargin;
304             buttons.innerHTML = btnmargin;
305             }
306             }
307            
308             /*
309             * Accordian function for folded code
310             *
311             * if clicked fold is closed
312             * replace contents of specified lineno margin span
313             * with complete lineno list
314             * replace contents of specified link span with end - start + 1 oplus's + linebreaks
315             * replace contents of insert span with contents of src_div
316             * foldstate = open
317             * else
318             * replace contents of specified lineno margin span with start lineno
319             * replace contents of specified link span with single oplus
320             * replace contents of specified insert span with "Folded lines start to end"
321             * foldstate = closed
322             *
323             * For fancier effect, use delay to add/remove a single line at a time, with
324             * delay millsecs between updates
325             */
326             ppiHtmlCF.prototype.accordian = function(startline, endline)
327             {
328             if (document.getElementById) {
329             if (navigator.appVersion.indexOf("MSIE")!=-1) {
330             this.ie_accordian(startline, endline);
331             }
332             else {
333             this.ff_accordian(startline, endline);
334             }
335             }
336             }
337             /*
338             * MSIE is a pile of sh*t, so we have to bend over
339             * backwards and completely rebuild the document elements
340             * Bright bunch of folks up there in Redmond...BTW this bug
341             * exists in IE 4 thru 7, despite people screaming for a solution
342             * for a decade. So MSFT is deaf as well as dumb
343             */
344             ppiHtmlCF.prototype.ie_accordian = function(startline, endline)
345             {
346             src = document.getElementById("preft" + startline);
347             foldbtn = document.getElementById('btnmargin');
348             lineno = document.getElementById('lnnomargin');
349             insert = document.getElementById("src" + startline);
350             linenos = lineno.innerHTML;
351             buttons = foldbtn.innerHTML;
352             if ((this.foldstate[startline] == null) || (this.foldstate[startline] == "closed")) {
353             lnr = "<span[^>]+>" + startline + "[\r\n]*</span>";
354             lnre = new RegExp(lnr, "i");
355             bnr = "id=ll" + startline + "[^<]+</a>";
356             btnre = new RegExp(bnr, "i");
357            
358             lnfill = startline + "\n";
359             btnfill = "&oplus;\n";
360             for (i = startline + 1; i <= endline; i++) {
361             lnfill += i + "\n";
362             btnfill += "&oplus;\n";
363             }
364            
365             linenos = linenos.replace(lnre, "<span id='lm" + startline + "' class='lnpre'>" + lnfill + "</span>");
366             buttons = buttons.replace(btnre, "id='ll" + startline + "' class='foldbtn' style='background-color: yellow' onclick=\"ppihtml.accordian(" +
367             startline + ", " + endline + ")\">" + btnfill + "</a>");
368            
369             foldbtn.outerHTML = "<pre id='btnmargin' class='lnpre'>" + buttons + "</pre>";
370             lineno.outerHTML = "<pre id='lnnomargin' class='lnpre'>" + linenos + "</pre>";
371             insert.outerHTML = "<span id='src" + startline + "'><pre class='bodypre'>" + src.innerHTML + "</pre></span>";
372             this.foldstate[startline] = "open";
373             }
374             else {
375             lnr = "<span[^>]+>" + startline + "[\r\n][^<]*</span>";
376             lnre = new RegExp(lnr, "i");
377             bnr = "id=ll" + startline + "[^<]+</a>";
378             btnre = new RegExp(bnr, "i");
379            
380             if (! linenos.match(lnre))
381             alert("linenos no match");
382             if (! buttons.match(btnre))
383             alert("buttons no match");
384             linenos = linenos.replace(lnre, "<span id='lm" + startline + "' class='lnpre'>" + startline + "\n</span>");
385             buttons = buttons.replace(btnre, "id='ll" + startline + "' class='foldbtn' style='background-color: #E9E9E9' onclick=\"ppihtml.accordian(" +
386             startline + ", " + endline + ")\">&oplus;\n</a>");
387            
388             foldbtn.outerHTML = "<pre id='btnmargin' class='lnpre'>" + buttons + "</pre>";
389             lineno.outerHTML = "<pre id='lnnomargin' class='lnpre'>" + linenos + "</pre>";
390             insert.outerHTML = "<span id='src" + startline + "'><pre class='foldfill'>Folded lines " + startline + " to " + endline + "</pre></span>";
391            
392             this.foldstate[startline] = "closed";
393             }
394             this.updateCookie();
395             }
396            
397             ppiHtmlCF.prototype.ff_accordian = function(startline, endline)
398             {
399             src = document.getElementById("preft" + startline);
400             foldbtn = document.getElementById("ll" + startline);
401             lineno = document.getElementById("lm" + startline);
402             insert = document.getElementById("src" + startline);
403             if ((this.foldstate[startline] == null) || (this.foldstate[startline] == "closed")) {
404             lnfill = startline + "\n";
405             btnfill = "&oplus;\n";
406             for (i = startline + 1; i <= endline; i++) {
407             lnfill += (i < endline) ? i + "\n" : i;
408             btnfill += (i < endline) ? "&oplus;\n" : "&oplus;";
409             }
410             foldbtn.innerHTML = btnfill;
411             lineno.innerHTML = lnfill;
412             foldbtn.style.backgroundColor = "yellow";
413             insert.innerHTML = src.innerHTML;
414             insert.className = "bodypre";
415             this.foldstate[startline] = "open";
416             }
417             else {
418             foldbtn.innerHTML = "&oplus;";
419             foldbtn.style.backgroundColor = "#E9E9E9";
420             lineno.innerHTML = startline;
421             insert.innerHTML = "Folded lines " + startline + " to " + endline;
422             insert.className = "foldfill";
423             this.foldstate[startline] = "closed";
424             }
425             this.updateCookie();
426             }
427            
428             /*
429             * open/close all folds
430             */
431             ppiHtmlCF.prototype.fold_all = function(foldstate)
432             {
433             for (i = 0; i < this.startlines.length; i++) {
434             line = this.startlines[i];
435             if (this.foldstate[line] == null)
436             this.foldstate[line] = "closed";
437            
438             if (this.foldstate[line] != foldstate)
439             this.accordian(line, this.endlines[i]);
440             }
441             this.updateCookie();
442             }
443            
444             ppiHtmlCF.prototype.add_fold = function(startline, endline)
445             {
446             this.startlines[this.startlines.length] = startline;
447             this.endlines[this.endlines.length] = endline;
448             this.foldstate[this.foldstate.length] = "closed";
449             }
450            
451             EOFTJS
452              
453             =pod
454            
455             =begin classdoc
456            
457             Constructor. Uses PPI::HTML base constructor, then installs some
458             additional members based on the <code>fold</code> argument.
459            
460             @optional colors hashref of <b>original</b> PPI::HTML classnames to color codes/names
461             @optional css a <cpan>CSS::Tiny</cpan> object containg additional stylsheet properties
462             @optional fold hashref of code folding properties; if not specified, a default
463             set of properties is applied. Folding properties include:
464             <ul>
465             <li>Abbreviate - hashref mapping full classnames to smaller classnames; useful
466             to provide further output compression; default uses predefined mapping
467             <li>Comments - if true, fold comments; default true
468             <li>Expandable - if true, provide links to unfold lines in place; default false
469             <li>Imports - if true, fold 'use' and 'require' statements; default false
470             <li>Javascript - name of file to reference for the fold expansion javascript in the output HTML;
471             default none, resulting in Javascript embedded in output HTML.<br>
472             Note that the Javascript may be retrieved separately via the <code>foldJavascript()</code> method.
473             <li>MinFoldLines - minimum number of consecutive foldable lines required before folding is applied;
474             default is 4
475             <li>POD - if true, fold POD line; default true
476             <li>Stylesheet - name of file to reference for the CSS for abbreviated classnames and
477             fold DIVs in the output HTML; default none,resulting in CSS embedded in output
478             HTML.<br>
479             Note that the CSS may be retrieved separately via the <code>foldCSS()</code> method.
480             <li>Tabs - size of tabs; default 4
481             </ul>
482            
483             @optional line_numbers if true, include line numbering in the output HTML
484             @optional page if true, wrap the output in a HTML &lt;head&gt; and &lt;body&gt;
485             sections. <b>NOTE: CodeFolder forces this to true.
486             @optional verbose if true, spews various diagnostic info
487            
488             @return a new PPI::HTML::CodeFolder object
489            
490             =end classdoc
491            
492             =cut
493              
494              
495             sub new {
496 2     2 1 591240     my ($class, %args) = @_;
497              
498 2         6     my $fold = delete $args{fold};
499 2         5     my $verb = delete $args{verbose};
500             #
501             # remove line numbering option since it greatly simplifies the spanning
502             # scan later; we'll apply it after we're done
503             #
504 2         7     my $needs_ln = delete $args{line_numbers};
505             #
506             # force page wrapping
507             #
508 2         4     $args{page} = 1;
509 2         21     my $self = $class->SUPER::new(%args);
510                 return undef
511 2 50       63         unless $self;
512              
513 2         6     $self->{_needs_ln} = $needs_ln;
514 2         5     $self->{_verbose} = $verb;
515 2 50       22     $self->{fold} = $fold ?
516                     { %$fold } :
517                     {
518                     Abbreviate => \%classabvs,
519                     Comments => 1,
520                     Heredocs => 0,
521                     Imports => 0,
522                     Javascript => undef,
523                     Expandable => 0,
524                     MinFoldLines => 4,
525                     POD => 1,
526                     Stylesheet => undef,
527                     Tabs => 4,
528                     };
529              
530 2 50 33     29     $self->{fold}{Abbreviate} = \%classabvs
531                     if $self->{fold}{Abbreviate} && (! (ref $self->{fold}{Abbreviate}));
532              
533 2 50       9     $self->{fold}{MinFoldLines} = 4
534                     unless $self->{fold}{MinFoldLines};
535              
536 2 50       12     $self->{fold}{Tabs} = 4
537                     unless $self->{fold}{Tabs};
538             #
539             # keep a running package/method cross reference
540             #
541 2         5 $self->{_pkgs} = {};
542              
543 2         8     return $self;
544             }
545              
546             =pod
547            
548             =begin classdoc
549            
550             Returns the Javascript used for fold expansion.
551            
552             @return Javascript for fold expansion, as a string
553            
554             =end classdoc
555            
556             =cut
557              
558 1     1 1 417 sub foldJavascript { return $ftjs; }
559              
560              
561             =pod
562            
563             =begin classdoc
564            
565             Write out the Javascript used for fold expansion.
566            
567             @return 1 on success; undef on failure, with error message in $@
568            
569             =end classdoc
570            
571             =cut
572              
573             sub writeJavascript {
574 0 0   0 1 0 $@ = $!,
575             return undef
576             unless open OUTF, ">$_[1]";
577 0         0 print OUTF $ftjs;
578 0         0 close OUTF;
579 0         0 return 1;
580             }
581              
582             =pod
583            
584             =begin classdoc
585            
586             Write out the CSS used for the sources.
587            
588             @return 1 on success; undef on failure, with error message in $@
589            
590             =end classdoc
591            
592             =cut
593              
594             sub writeCSS {
595 0 0   0 1 0 $@ = $!,
596             return undef
597             unless open OUTF, ">$_[1]";
598 0         0 print OUTF $_[0]->foldCSS();
599 0         0 close OUTF;
600 0         0 return 1;
601             }
602              
603             =pod
604            
605             =begin classdoc
606            
607             Returns the CSS used for the abbreviated classes and fold DIVs.
608            
609             @return CSS as a string
610            
611             =end classdoc
612            
613             =cut
614              
615              
616             sub foldCSS {
617 2     2 1 558     my $self = shift;
618 2         6     my $orig_colors = exists $self->{colors};
619 2   50     20     my $css = $self->_css_html() || << 'EOCSS';
620             <style type="text/css">
621             <!--
622             body {
623             font-family: fixed, Courier;
624             font-size: 10pt;
625             }
626             EOCSS
627              
628 2         655 my $ftc = $ftcss;
629 2 50       8 if ($self->{colors}{line_number}) {
630 2         5 my $lnc = $self->{colors}{line_number};
631 2         9 $ftc=~s/(.lnpre\s+.+?color: )#888888;/$1$lnc;/gs;
632             }
633              
634 2 50       8     delete $self->{colors} unless $orig_colors;
635 2         19     $css=~s|-->\s*</style>||s;
636             #
637             # !!!fix for (yet another) Firefox bug: need a dummy class
638             # at front of CSS or firefox ignores the first class...
639             #
640 2         22 $css=~s/(<!--.*?\n)/$1\n\n.dummy_class_for_firefox { color: white; }\n/;
641             #
642             # replace classes w/ abbreviations
643             #
644 2 50       10     if ($self->{fold}{Abbreviate}) {
645 2         4         my ($long, $abv);
646 70         930         $css=~s/\.$long \{/.$abv {/s
647 2         5             while (($long, $abv) = each %{$self->{fold}{Abbreviate}});
648                 }
649 2         14     return $css . $ftc;
650             }
651              
652             =pod
653            
654             =begin classdoc
655            
656             Generate folded HTML from source PPI document.
657             Overrides base class <code>html()</code> to apply codefolding support.
658            
659             @param $src a <cpan>PPI::Document</cpan> object, OR the
660             path to the source file, OR a scalarref of the
661             actual source text.
662             @optional $outfile name of the output HTML file; If not specified for a filename $src, the
663             default is "$src.html"; If not specified for either PPI::Document or text $src,
664             defaults to an empty string.
665             @optional $script a name used if source is a script file. Script files might not include
666             any explicit packages or method declarations which would be mapped into the
667             table of contents. By specifying this parameter, an entry is forced into the
668             table of contents for the script, with any "main" package methods within the
669             script reassigned to this script name. If not specified, and <code>$src</code>
670             is not a filename, an error will be issued when the TOC is generated.
671            
672             @return on success, the folded HTML; undef on failure
673            
674             @returnlist on success, the folded HTML and a hashref mapping packages to an arrayref of method names;
675             undef on failure
676            
677             =end classdoc
678            
679             =cut
680              
681             sub html {
682 2     2 1 920     my ($self, $src, $outfile, $script) = @_;
683              
684 2         6     my $orig_colors = exists $self->{colors};
685 2 50       15     my $html = $self->SUPER::html($src)
686                     or return undef;
687              
688 2 0       1034607 $outfile = (ref $src) ? '' : "$src.html"
    50          
689             unless $outfile;
690 2 50 0     16 $script ||= $src
      33        
691             unless ref $src || (substr($src, -3) eq '.pm');
692             #
693             # expand tabs as needed (we use 4 space tabs)
694             # have to adjust some spans that confuse tab processing
695             #
696 2         1053 my @lns = split /\n/, $html;
697 2         91 my $tabsz = $self->{fold}{Tabs};
698 2         11 foreach my $line (@lns) {
699 2734 50       6415 next if $line=~s/^\s*$//;
700 2734 100       5563 next unless $line=~tr/\t//;
701 1184         1291 my $offs = 0;
702 1184         1052 my $pad;
703             #
704             # scan for and replace tabs; adjust positions
705             # of extracted tags as needed
706             #
707 1184         2276 pos($line) = 0;
708 1184         3883 while ($line=~/\G.*?((<[^>]+>)|\t)/gc) {
709 7210 100       26736 $offs += length($2),
710             next
711             unless ($1 eq "\t");
712              
713 1976         3711 $pad = $tabsz - ($-[1] - $offs) % $tabsz;
714 1976         5313 substr($line, $-[1], 1, ' ' x $pad);
715 1976         13257 pos($line) = $-[1] + $pad - 1;
716             }
717             }
718 2         725 $html = join("\n", @lns);
719              
720 2 50       19     delete $self->{colors} unless $orig_colors;
721              
722 2         10     my $opts = $self->{fold};
723             #
724             # extract stylesheet and replace with abbreviated version
725             #
726 2 100       26 my $style = $opts->{Stylesheet} ?
727                     "<link type='text/css' rel='stylesheet' href='" .
728                      _pathAdjust($outfile, $opts->{Stylesheet}) . "' />" :
729                     $self->foldCSS();
730              
731 2 100       91     $style .= $opts->{Javascript} ?
    50          
732                     "\n<script type='text/javascript' src='" .
733                      _pathAdjust($outfile, $opts->{Javascript}) . "'></script>\n" :
734                     "\n<script type='text/javascript'>\n$ftjs\n</script>\n"
735                     if $opts->{Expandable};
736             #
737             # original html may have no style, so we've got to add OR replace
738             #
739 2 50       2004     $html=~s|</head>|$style</head>|s
740                     unless ($html=~s|<style type="text/css">.+</style>|$style|s);
741             #
742             # force spans to end before line endings
743             #
744 2         1407 $html=~s!(<br>\s*)</span>!</span>$1!g;
745             #
746             # split multiline comments into 2 spans: 1st line (in case its midline)
747             # and the remainder; note that the prior substitution avoids
748             # doing this to single line comments
749             #
750 2         997 $html=~s/(?!<br>\s+)(<span class=['"]comment['"]>[^<]+)<br>\n/$1<\/span><br>\n<span class="comment">/g;
751             #
752             # keep folded fragments here for later insertion
753             # as fold DIVs; key is starting line number,
754             # value is [ number of lines, text ]
755             #
756 2         17     my %folddivs = ( 1 => [ 0, '', 0, 0 ]);
757             #
758             # count <br> tags, and looks for any of
759             # comment, pod, or use/require keyword (depending on the options);
760             # keeps track of start and end position of foldable segments
761             #
762 2         4     my $lineno = 1;
763 2         5     my $lastfold = 1;
764              
765 2         801 $html=~s/<br>\n/<br>/g;
766             #
767             # now process remainder
768             #
769 2         6 study $html;
770 2         8     pos($html) = 0;
771 2         50     $html=~/^.*?(<body[^>]+><pre>)/s;
772 2         8 my $startpos = $+[1];
773             #
774             # map linebreak positions to line numbers
775             #
776 2         6 my @lnmap = (0, $startpos);
777 2         6845 push @lnmap, $+[1]
778             while ($html=~/\G.*?(<br>)/gcs);
779             #
780             # now scan for foldables
781             #
782 2         9 pos($html) = $startpos;
783 2         20 my @folds = _extractFolds(\$html, $startpos, \@lnmap, $opts);
784             #
785             # trim small folds;
786             # since its used frequently, create a sorted list of the fold DIV lines;
787             # isolate positions of folds and extract folded content
788             #
789 2         17 my $ln = 0;
790 2         4 my @ftsorted = ();
791 2         6 foreach (@folds) {
792 212 100       483 if ($_->[1] - $_->[0] + 1 >= $opts->{MinFoldLines}) {
    50          
793 60         329 $folddivs{$_->[0]} = [ $_->[1], substr($html, $lnmap[$_->[0]], $lnmap[$_->[1] + 1] - $lnmap[$_->[0]]),
794             $lnmap[$_->[0]], $lnmap[$_->[1] + 1] ];
795 60         92 push @ftsorted, $_->[0];
796             }
797             elsif ($self->{_verbose}) {
798             # print "*** skipping section at line $_->[0]to $_->[1]\n";
799             # print substr($html, $lnmap[$_->[0]], $lnmap[$_->[1] + 1] - $lnmap[$_->[0]]), "\n";
800             }
801             }
802             #
803             # now remove the folded lines; we work from bottom to top since
804             # we're changing the HTML as we go, which would invalidate the
805             # positional elements we've kept. If fold expansion is enabled, we replace
806             # w/ a hyperlink; otherwise we replace with a simple indication of the fold
807             #
808             substr($html, $folddivs{$_}[2], $folddivs{$_}[3] - $folddivs{$_}[2],
809             "<span id='src$_' class='foldfill'>Folded lines $_ to " . $folddivs{$_}[0] . "</span>\n")
810 2         1178      foreach (reverse @ftsorted);
811             #
812             # abbreviate the default span classes for both the html and fold divs
813             #
814 2         8     pos($html) = 0;
815 2         6     my $abvs = $opts->{Abbreviate};
816 2 50       9     if ($abvs) {
817 2   33     96         $html=~s/(<span\s+class=['"])([^'"]+)(['"])/$1 . ($$abvs{$2} || $2) . $3/egs;
  2774         28499  
818 2 50       17         if ($opts->{Expandable}) {
819 108   33     1054             $_->[1]=~s/(<span\s+class=['"])([^'"]+)(['"])/$1 . ($$abvs{$2} || $2) . $3/egs
820 2         33                 foreach (values %folddivs);
821                     }
822                 }
823             #
824             # create and insert fold DIVs if requested
825             #
826 2 50       24     my $expdivs = $opts->{Expandable} ? _addFoldDivs(\%folddivs, \@ftsorted) : '';
827              
828 2         670     $html=~s/<br>/\n/gs;
829             #
830             # now create the line number table (if requested)
831             # NOTE: this is where having the breakable lines would be really
832             # useful!!!
833             #
834 2 50       25 _addLineNumTable(\$html, \@ftsorted, \%folddivs, \$expdivs, $#lnmap)
835             if $self->{_needs_ln};
836             #
837             # extract a package/method reference list, and add anchors for them
838             #
839 2         14 $self->_extractXRef(\$html, $outfile, $script);
840             #
841             # report number of spans, for firefox performance report
842             #
843 2 50       9 if ($self->{_verbose}) {
844 0         0 my $spancnt = $html=~s/<\/span>/<\/span>/gs;
845 0         0 print "\n***Total spans: $spancnt\n";
846             }
847             #
848             # fix Firefox blank lines inside spans bug: add a single space to
849             # all blank lines
850             #
851 2         441 $html=~s!\n\n!\n \n!gs;
852              
853 2         1295     return $html;
854             }
855              
856             =pod
857            
858             =begin classdoc
859            
860             Return current package/method cross reference.
861            
862             @return hashref of current package/method cross reference
863            
864             =end classdoc
865            
866             =cut
867              
868 1     1 1 3875 sub getCrossReference { return $_[0]->{_pkgs}; }
869              
870             =pod
871            
872             =begin classdoc
873            
874             Write out a table of contents document for the current collection of
875             sources as a nested HTML list. The output filename is 'toc.html'.
876             The caller may optionally specify the order of packages in the menu.
877            
878             @param $path directory to write TOC file
879             @optional Order arrayref of packages in the order in which they should appear in TOC; if a partial list,
880             any remaining packages will be appended to the TOC in alphabetical order
881            
882             @return this object on success, undef on failure, with error message in $@
883            
884             =end classdoc
885            
886             =cut
887              
888             sub writeTOC {
889 1     1 1 1559 my $self = shift;
890 1         6 my $path = shift;
891 1 50       315 $@ = "Can't open $path/toc.html: $!",
892             return undef
893             unless CORE::open(OUTF, ">$path/toc.html");
894              
895 1         13 print OUTF $self->getTOC("$path/toc.html", @_);
896 1         126 close OUTF;
897 1         7 return $self;
898             }
899              
900             =begin classdoc
901            
902             Generate a table of contents document for the current collection of
903             sources as a nested HTML list. Caller may optionally specify
904             the order of packages in the menu.
905            
906             @param $tocpath path of output TOC file
907             @optional Order arrayref of packages in the order in which they should appear in TOC; if a partial list,
908             any remaining packages will be appended to the TOC in alphabetical order
909            
910             @return the TOC document
911            
912             =end classdoc
913            
914             =cut
915              
916             sub getTOC {
917 3     3 1 2295 my $self = shift;
918 3         29 my $tocpath = shift;
919 3         21 my %args = @_;
920 3 100       24 my @order = $args{Order} ? @{$args{Order}} : ();
  2         24  
921 3         15 my $sources = $self->{_pkgs};
922 3         9 my $base;
923 3         12 my $doc =
924             "<html>
925             <body>
926             <small>
927             <!-- INDEX BEGIN -->
928             <ul>
929             ";
930 3         12 my %ordered = ();
931 3         27 $ordered{$_} = 1 foreach (@order);
932 3         23 foreach (sort keys %$sources) {
933 3 100       22 push @order, $_ unless exists $ordered{$_};
934             }
935              
936 3         13 foreach my $class (@order) {
937             #
938             # due to input @order, we might get classes that don't exist
939             #
940 5 100       27 next unless exists $sources->{$class};
941              
942 3         22 $base = _pathAdjust($tocpath, $sources->{$class}{URL});
943 3         17 $doc .= "<li><a href='$base' target='mainframe'>$class</a>
944             <ul>\n";
945 3         12 my $info = $sources->{$class}{Methods};
946             $doc .= "<li><a href='" . _pathAdjust($tocpath, $info->{$_}) . "' target='mainframe'>$_</a></li>\n"
947 3         80 foreach (sort keys %$info);
948 3         17 $doc .= "</ul>\n</li>\n";
949             }
950              
951 3         9 $doc .= "
952             </ul>
953             <!-- INDEX END -->
954             </small>
955             </body>
956             </html>
957             ";
958              
959 3         46 return $doc;
960             }
961              
962             =pod
963            
964             =begin classdoc
965            
966             Write out a frame container document to hold the rendered source and TOC.
967             The file is written to "$path/index.html".
968            
969             @param $path directory to write the document.
970             @param $title Title string for resulting document
971             @optional $home the "home" document initially loaded into the main frame; default none
972            
973             @return this object on success, undef on failure, with error message in $@
974            
975             =end classdoc
976            
977             =cut
978              
979             sub writeFrameContainer {
980 1     1 1 813 my ($self, $path, $title, $home) = @_;
981 1 50       137 $@ = "Can't open $path/index.html: $!",
982             return undef
983             unless open(OUTF, ">$path/index.html");
984              
985 1         7 print OUTF $self->getFrameContainer($title, $home);
986 1         52 close OUTF;
987 1         6 return $self;
988             }
989              
990             =begin classdoc
991            
992             Generate a frame container document to hold the rendered source and TOC.
993            
994             @return the frame container document as a string
995            
996             =end classdoc
997            
998             =cut
999              
1000             sub getFrameContainer {
1001 2     2 1 807 my ($self, $title, $home) = @_;
1002 2 50       27 return $home ?
1003             "<html><head><title>$title</title></head>
1004             <frameset cols='15%,85%'>
1005             <frame name='navbar' src='toc.html' scrolling=auto frameborder=0>
1006             <frame name='mainframe' src='$home'>
1007             </frameset>
1008             </html>
1009             " :
1010             "<html><head><title>$title</title></head>
1011             <frameset cols='15%,85%'>
1012             <frame name='navbar' src='toc.html' scrolling=auto frameborder=0>
1013             <frame name='mainframe'>
1014             </frameset>
1015             </html>
1016             ";
1017             }
1018             #
1019             # extract a package/method reference list, and add anchors for them
1020             #
1021             sub _extractXRef {
1022 2     2   9 my ($self, $html, $outfile, $script) = @_;
1023 2 50       11 $self->{_pkgs} = {} unless exists $self->{_pkgs};
1024 2         7 my $pkgs = $self->{_pkgs};
1025 2         4 my $pkglink;
1026             #
1027             # assume package "main" to start; on exit,
1028             # if we have a script name, then replace all "main"
1029             # entries with $script
1030             #
1031 2         4 my $curpkg = 'main';
1032 2 50       6 $pkgs->{main} = {
1033             URL => $outfile,
1034             Methods => {}
1035             }
1036             if $script;
1037              
1038 2         186 while ($$html=~/\G.*?(<span class=['"]kw['"]>)\s*(package|sub)\s*<\/span>\s*(<span class=['"][^'"]+['"]>\s*)?([\w:]+)/gcs) {
1039             # " to keep Textpad formatting happy
1040 40         88 my $pkg = $4;
1041 40         51 my $next = pos($$html);
1042 40         88 my $insert = $-[1];
1043 40 100       103 if ($2 eq 'package') {
1044 2         45 $curpkg = $pkg;
1045 2 50 33     15 next if exists $pkgs->{$pkg} && $pkgs->{$pkg}{URL}; # only use 1st definition of package
1046 2         6 $pkglink = $pkg;
1047 2         17 $pkgs->{$pkg} = {
1048             URL => "$outfile#$pkg",
1049             Methods => {}
1050             };
1051             }
1052             else {
1053 38 50       88 if ($pkg=~/^(.+)::(\w+)$/) {
1054             #
1055             # fully qualified name, check if we have a pkg entry for it
1056             #
1057 0 0       0 $pkgs->{$1} = {
1058             URL => '',
1059             Methods => {}
1060             }
1061             unless exists $pkgs->{$1};
1062 0         0 $pkgs->{$1}{Methods}{$2} = "$outfile#$pkg";
1063 0         0 $pkglink = $pkg;
1064             }
1065             else {
1066 38 50       93 $pkglink = ($curpkg eq 'main') ? $pkg : "$curpkg\:\:$pkg";
1067 38         167 $pkgs->{$curpkg}{Methods}{$pkg} = "$outfile#$pkglink";
1068             }
1069             }
1070 40         61 $pkglink = "<a name='$pkglink'></a>";
1071 40         2986 substr($$html, $insert, 0, $pkglink);
1072 40         59 $next += length($pkglink);
1073 40         1391 pos($$html) = $next;
1074             }
1075 2 50       11 $pkgs->{$script} = delete $pkgs->{main}
1076             if $script;
1077 2         7 return $html;
1078             }
1079              
1080             sub _extractFolds {
1081 2     2   7 my ($html, $startpos, $lnmap, $opts) = @_;
1082             #
1083             # scan for foldables
1084             #
1085 2         8 pos($$html) = $startpos;
1086 2         25 my %folded = (
1087             Whitespace => [],
1088             Comments => [],
1089             POD => [],
1090             Heredocs => [],
1091             Imports => [],
1092             );
1093 2         5 my $whitespace = [];
1094             #
1095             # accumulate foldable sections, including leading/trailing whitespace
1096             #
1097             # my $fre = $foldres{$_}[0];
1098             # push @{$folded{$_}}, [ $-[1], $+[1] - 1 ]
1099             # if ($$html=~/$fre/gcs);
1100              
1101             # push @{$folded{Whitespace}}, [ $-[1], $+[1] - 1 ]
1102             # while ($$html=~/\G.*?<br>((?:\s*<br>)+)/gcs);
1103             # _mergeSection(_cvtToLines($folded{Whitespace}, $lnmap))
1104             # if scalar @{$folded{Whitespace}};
1105              
1106 2         4 pos($$html) = $startpos;
1107 2         9 foreach (qw(Whitespace Comments POD Heredocs Imports)) {
1108 10 50 66     64 next unless ($_ eq 'Whitespace') || $opts->{$_};
1109             #
1110             # capture anything at the very beginning
1111             #
1112 10         36 my $fre = $foldres{$_}[0];
1113 10 100       145     push @{$folded{$_}}, [ $-[1], $+[1] - 1 ]
  2         13  
1114             if ($$html=~/$fre/gcs);
1115            
1116 10         24 $fre = $foldres{$_}[1];
1117 10         1261     push @{$folded{$_}}, [ $-[1], $+[1] - 1 ]
  544         11179  
1118             while ($$html=~/$fre/gcs);
1119 10         58 _mergeSection(_cvtToLines($folded{$_}, $lnmap))
1120 10 50       17 if scalar @{$folded{$_}};
1121 10         46 pos($$html) = $startpos;
1122             }
1123             #
1124             # now merge different sections
1125             #
1126 2         6 my $last = 'Whitespace';
1127 2         7 foreach (qw(Imports POD Heredocs Comments)) {
1128 8         27 _mergeSections($folded{$_}, $folded{$last});
1129 8         14 $last = $_;
1130             }
1131 2         3 return @{$folded{$last}};
  2         110  
1132             }
1133              
1134             sub _cvtToLines {
1135 10     10   27 my ($pos, $lnmap) = @_;
1136            
1137 10         14 my $ln = 1;
1138 10         23 foreach (@$pos) {
1139 546   66     25038 $ln++ while ($ln <= $#$lnmap) && ($lnmap->[$ln] <= $_->[0]);
1140 546         655 $_->[0] = $ln - 1;
1141 546   66     5725 $ln++ while ($ln <= $#$lnmap) && ($lnmap->[$ln] <= $_->[1]);
1142 546         784 $_->[1] = $ln - 1;
1143             }
1144 10         51 return $pos;
1145             }
1146              
1147             sub _mergeSection {
1148 18     18   31 my $sect = shift;
1149 18         42 my @temp = shift @$sect;
1150 18         37 foreach (@$sect) {
1151 1600 100       3724 push(@temp, $_),
1152             next
1153             unless ($temp[-1][1] + 1 >= $_->[0]);
1154             #
1155             # if current surrounds new, the discard new
1156             #
1157 334 100       897 $temp[-1][1] = $_->[1]
1158             if ($temp[-1][1] < $_->[1]);
1159             }
1160 18         316 @$sect = @temp;
1161 18         104 1;
1162             }
1163              
1164             sub _mergeSections {
1165 8     8   14 my ($first, $second) = @_;
1166            
1167 8 50       18 if ($#$first < 0) {
1168 0         0 @$first = @$second;
1169 0         0 return $first;
1170             }
1171              
1172 8         34 my @temp = ();
1173 8 100 66     2429 push @temp, (($first->[0][0] < $second->[0][0]) ? shift @$first : shift @$second)
1174             while (@$first && @$second);
1175              
1176 8 50       18 push @temp, @$first if scalar @$first;
1177 8 50       276 push @temp, @$second if scalar @$second;
1178 8         19 _mergeSection(\@temp);
1179 8         94 @$first = @temp;
1180 8         38 1;
1181             }
1182              
1183             sub _addLineNumTable {
1184 2     2   7 my ($html, $ftsorted, $folddivs, $expdivs, $linecnt) = @_;
1185              
1186 2         72 $$html=~s/<pre>/<pre class='bodypre'>/;
1187 2         22 $$html=~/(<body[^>]+>)/s;
1188 2         8 my $insert = $+[0];
1189             #
1190             # generate JS declaration of fold sections
1191             #
1192 2 50       75 my $startfolds = scalar @$ftsorted ?
1193             '[' . join(',', @$ftsorted) . " ],\n[" . join(',', map $folddivs->{$_}[0], @$ftsorted) . " ]" :
1194             "[], []";
1195              
1196 2         122 my $linenos = $$expdivs . "
1197             <table border=0 width='100\%' cellpadding=0 cellspacing=0>
1198             <tr>
1199             <td width=40 bgcolor='#E9E9E9' align=right valign=top>
1200             <pre id='lnnomargin' class='lnpre'>
1201             </pre>
1202             </td>
1203             <td width=8 bgcolor='#E9E9E9' align=right valign=top>
1204             <pre id='btnmargin' class='lnpre'>
1205             </pre>
1206             </td>
1207             <td bgcolor='white' align=left valign=top>
1208             ";
1209 2         303 substr($$html, $insert, 0, $linenos);
1210 2         143 substr($$html, index($$html, '</body>'), 0, "
1211             </td></tr></table>
1212            
1213             <script type='text/javascript'>
1214             <!--
1215            
1216             var ppihtml = new ppiHtmlCF($startfolds);
1217             ppihtml.renderMargins($linecnt);
1218             /*
1219             * all rendered, now selectively open from any existing cookie
1220             */
1221             ppihtml.openFromCookie();
1222            
1223             -->
1224             </script>
1225             "
1226             );
1227 2         5 return 1;
1228             }
1229              
1230             sub _addFoldDivs {
1231 2     2   6 my ($folddivs, $ftsorted) = @_;
1232 2         10 foreach my $ft (values %$folddivs) {
1233 60         587 $ft->[1]=~s/<br>/\n/gs;
1234             #
1235             # squeeze out leading whitespace, but keep aligned
1236             #
1237 60         68 my $shortws = 1000000;
1238 60         571 my @lns = split /\n/, $ft->[1];
1239             #
1240             # expand tabs as needed (we use 4 space tabs)
1241             #
1242 60         123 foreach (@lns) {
1243 88 100       242 next if s/^\s*$//;
1244 60 50       146 $shortws = 0, last
1245             unless /^(\s+)/;
1246 0 0       0 $shortws = length($1)
1247             if ($shortws > length($1))
1248             }
1249 60 0       94 $ft->[1] = join("\n", map { $_ ? substr($_, $shortws) : ''; } @lns)
  0 50       0  
1250             if $shortws;
1251             #
1252             # move whitespace inside any leading/trailing spans
1253             #
1254 60         337 $ft->[1]=~s!(</span>)(\s+)$!$2$1!s;
1255 60         246 $ft->[1]=~s!^(\s+)(<span [^>]+>)!$2$1!s;
1256             #
1257             # if ends on span, make sure its not creating newline
1258             #
1259 60         305 $ft->[1]=~s!\n</span>$! </span>!s;
1260             #
1261             # likewise if it doesn't end on a span
1262             #
1263 60         231 $ft->[1]=~s!\n$!!s;
1264             }
1265 2         148 return join('', map "\n<div id='ft$_' class='folddiv'><pre id='preft$_'>$folddivs->{$_}[1]</pre></div>", @$ftsorted);
1266             }
1267              
1268             sub _pathAdjust {
1269 62     62   150 my ($path, $jspath) = @_;
1270 62 50 33     519 return $jspath
1271             unless (substr($jspath, 0, 2) eq './') && (substr($path, 0, 2) eq './');
1272             #
1273             # relative path, adjust as needed from current base
1274             #
1275 0           my @parts = split /\//, $path;
1276 0           my @jsparts = split /\//, $jspath;
1277 0           my $jsfile = pop @jsparts; # get rid of filename
1278 0           pop @parts; # remove filename
1279 0           shift @parts;
1280 0           shift @jsparts; # and the relative lead
1281 0           my $prefix = '';
1282 0   0       shift @parts,
      0        
1283             shift @jsparts
1284             while @parts && @jsparts && ($parts[0] eq $jsparts[0]);
1285 0           push @jsparts, $jsfile;
1286 0           return ('../' x scalar @parts) . join('/', @jsparts)
1287             }
1288              
1289             1;
1290