#!/usr/bin/env perl # util/smartlinks.pl - The successor to util/catalog_tests.pl. # This program is still under active development, and # you're very welcome to improve it. # Please read the Pod documentation at the end of file before reading and/or # modifying the source. # CAUTION: please make sure your changes don't break anything, because # breakage of this script will also break http://perlcabal.org/syn/ # immediately. Running *-smartlinks.t under util/t/ before committing is # strongly recommended. Thank you for your contribution :) use strict; #use warnings; #use Smart::Comments; #use YAML::Syck; use Getopt::Long; use File::Basename; use FindBin; use File::Find qw(find); #use File::Slurp; #use Pod::Simple::HTML; my $check; my $test_result; my $line_anchor; my ($syn_rev, $pugs_rev, $smoke_rev); my ($link_count, $broken_link_count); my (@snippets, $snippet_id); my %Spec = reverse qw( 01 Overview 02 Syntax 03 Operator 04 Block 05 Rule 06 Subroutine 09 Structure 10 Package 11 Module 12 Object 13 Overload 16 IO 17 Concurrency 22 CPAN 26 Documentation 29 Functions ); my $javascript = ''; # EVENT HANDLING $javascript .= <<'_EOC_'; // http://therealcrisp.xs4all.nl/upload/addEvent_dean.html // written by Dean Edwards, 2005 // with input from Tino Zijdel - crisp@xs4all.nl // http://dean.edwards.name/weblog/2005/10/add-event/ // modified by Aankhen var addEvent; if (window.addEventListener) { addEvent = function (element, type, handler) { element.addEventListener(type, handler, (typeof arguments[3] != 'undefined') ? arguments[3] : false); }; } else { addEvent = function (element, type, handler) { // assign each event handler a unique ID if (!handler.$$guid) handler.$$guid = addEvent.guid++; // create a hash table of event types for the element if (!element.events) element.events = {}; // create a hash table of event handlers for each element/event pair var handlers = element.events[type]; if (!handlers) { handlers = element.events[type] = {}; // store the existing event handler (if there is one) if (element['on' + type]) { handlers[0] = element['on' + type]; } // assign a global event handler to do all the work element['on' + type] = handleEvent; } // store the event handler in the hash table handlers[handler.$$guid] = handler; } } // a counter used to create unique IDs addEvent.guid = 1; function removeEvent(element, type, handler) { if (element.removeEventListener) element.removeEventListener(type, handler, false); // delete the event handler from the hash table else if (element.events && element.events[type] && handler.$$guid) delete element.events[type][handler.$$guid]; } function handleEvent(event) { // grab the event object (IE uses a global event object) event = event || fixEvent(window.event); var returnValue = true; // get a reference to the hash table of event handlers var handlers = this.events[event.type]; // execute each event handler for (var i in handlers) { // don't copy object properties if (!Object.prototype[i]) { this.$$handler = handlers[i]; if (this.$$handler(event) === false) { returnValue = false; // in accordance with DOM2-Events, all remaining event handlers on the object will be triggered, hence the absence of a `break` } } } // clean up if (this.$$handler) this.$$handler = null; return returnValue; } function fixEvent(event) { // add W3C standard event methods event.preventDefault = fixEvent.preventDefault; event.stopPropagation = fixEvent.stopPropagation; return event; } fixEvent.preventDefault = function() { this.returnValue = false; }; fixEvent.stopPropagation = function() { this.cancelBubble = true; }; _EOC_ # VISIBILITY TOGGLE $javascript .= <<'_EOC_'; function toggle_snippet (e) { var matches = this.id.match(/smartlink_toggle(\d+)/); var num = matches[1]; var id = 'smartlink_' + num; var div = document.getElementById(id); div.style.display = (div.style.display == 'none') ? '' : 'none'; var text = this.firstChild; text.nodeValue = text.nodeValue.replace(/^- (Show|Hide)/, function (full, p1) { return "- " + ((p1 == 'Show') ? 'Hide' : 'Show') }); // this may be unnecessarily complicated, or it may not. you get to decide. :-) e.stopPropagation(); e.preventDefault(); return false; } _EOC_ # LINK GENERATION # this would be simpler if we used a library like YUI to simplify retrieval and creation of elements, but oh well $javascript .= <<'_EOC_'; function collectionToArray(col) { a = new Array(); for (i = 0; i < col.length; i++) a[a.length] = col[i]; return a; } addEvent(window, 'load', function () { var divs = collectionToArray(document.getElementsByTagName('div')); for (var i = 0, j = divs.length; i < j; i++) { var curr = divs[i]; if (curr.id && curr.id.match(/smartlink_(\d+)/)) { var num = RegExp.$1; var toBeRemoved = [ "smartlink_skip_", "smartlink_skipto_" ]; // let it be reusable since this list could conceivably grow :-) for (var k = 0, l = toBeRemoved.length; k < l; k++) { var id = toBeRemoved[k] + num; var elm = document.getElementById(id); elm.parentNode.removeChild(elm); } var p = curr.previousSibling; while (p.nodeType != 1) { p = p.previousSibling; } // ignore any whitespace-only nodes var text = p.firstChild; text.nodeValue = text.nodeValue.replace(/^From/, '- Show'); var link = document.createElement('a'); var child; while (child = p.firstChild) { link.appendChild(child); } var end = link.lastChild; if ((end.nodeType == 3) && (end.nodeValue.search(/:$/) > -1)) { end.nodeValue = end.nodeValue.replace(/:$/, ' -'); } link.href = '#'; link.id = 'smartlink_toggle' + num; addEvent(link, 'click', toggle_snippet); p.appendChild(link); curr.parentNode.insertBefore(p, curr); curr.style.display = 'none'; } // explicitly jump to the page anchor (if any) since the code above messes it up if (location.hash && location.hash.match(/#.+/)) location.hash = RegExp.lastMatch; } }); _EOC_ =begin private =head2 add_link add_link($linktree, $synopsis, $section, $pattern, $infile, $from, $to); Side Effects: - modifies global C<$link_count> =end private =cut sub add_link ($$$$$$$) { my ($linktree, $synopsis, $section, $pattern, $t_file, $from, $to) = @_; if ($from == $to) { warn "WARNING: empty snippet detected at $t_file (line $from ~ $to).\n"; } $linktree->{$synopsis} ||= {}; $linktree->{$synopsis}->{$section} ||= []; if ($pattern and substr($pattern, -1, 1) eq '/') { $pattern = "/$pattern"; } push @{ $linktree->{$synopsis}->{$section} }, [$pattern => [$t_file, $from, $to]]; $link_count++; } sub error { if ($check) { warn "ERROR: @_\n"; } } sub process_t_file ($$) { my ($infile, $linktree) = @_; open my $in, $infile or die "error: Can't open $infile for reading: $!\n"; my ($setter, $from, $to); while (<$in>) { chomp; my $new_from; my ($synopsis, $section, $pattern); if (/^ \s* \#? \s* L< (S\d+) \/ ([^\/]+) >\s*$/xo) { ($synopsis, $section) = ($1, $2); $section =~ s/^\s+|\s+$//g; $section =~ s/^"(.*)"$/$1/; #warn "$synopsis $section" if $synopsis eq 'S06'; $new_from = $.; $to = $. - 1; } elsif (/^ \s* \#? \s* L(<) (S\d+) \/ ([^\/]+) \/ (.*) /xo) { #warn "$1, $2, $3\n"; my $brackets; ($brackets, $synopsis, $section, $pattern) = ($1, $2, $3, $4); $brackets = length($brackets); $section =~ s/^\s+|\s+$//g; $section =~ s/^"(.*)"$/$1/; if (!$section) { error "$infile: line $.: section name can't be empty."; } $pattern =~ s/^\s+|\s+$//g; if (substr($pattern, -1, 1) ne '>') { $_ = <$in>; s/^\s*\#?\s*|\s+$//g; if (!s/>{$brackets}$//) { error "$infile: line $.: smart links must terminate", "in the second line."; next; } $pattern .= " $_"; $new_from = $. - 1; $to = $. - 2; } else { $new_from = $.; $to = $. - 1; $pattern =~ s/\s*>{$brackets}$//; } #warn "*$synopsis* *$section* *$pattern*\n"; } elsif (/^ \s* \#? \s* L S\d+\b /xoi) { error "$infile: line $.: syntax error in the magic link:\n\t$_"; } else { next; } #warn "*$synopsis* *$section*\n"; if ($from and $from == $to) { my $old_setter = $setter; my $old_from = $from; $setter = sub { add_link($linktree, $synopsis, $section, $pattern, $infile, $_[0], $_[1]); $old_setter->($old_from, $_[1]); #warn "$infile - $old_from ~ $_[1]"; }; #warn "$infile - $from ~ $to"; } else { $setter->($from, $to) if $setter and $from; $setter = sub { add_link($linktree, $synopsis, $section, $pattern, $infile, $_[0], $_[1]); }; } $from = $new_from; } $setter->($from, $.) if $setter and $from; close $in; } sub parse_pod ($) { my $infile = shift; open my $in, $infile or die "can't open $infile for reading: $!\n"; my $podtree = {}; my $section; while (<$in>) { if (/^ =head(\d+) \s* (.*\S) \s* $/x) { #warn "parse_pod: *$1*\n"; my $num = $1; $section = $2; $podtree->{_sections} ||= []; push @{ $podtree->{_sections} }, [$num, $section]; } elsif (!$section) { $podtree->{_header} .= $_; } elsif (/^\s*$/) { $podtree->{$section} ||= []; #push @{ $podtree->{$section} }, "\n"; my @new = ('');; if ($line_anchor and $podtree->{$section}->[-1] !~ /^=over\b|^=item\b/) { unshift @new, "_LINE_ANCHOR_$.\n"; } push @{ $podtree->{$section} }, @new; } elsif (/^\s+(.+)/) { $podtree->{$section} ||= ['']; $podtree->{$section}->[-1] .= $_; push @{ $podtree->{$section} }, ''; } else { $podtree->{$section} ||= ['']; $podtree->{$section}->[-1] .= $_; } } close $in; $podtree; } sub emit_pod ($) { my $podtree = shift; my $str; $str .= $podtree->{_header} if $podtree->{_header}; for my $elem (@{ $podtree->{_sections} }) { my ($num, $sec) = @$elem; $str .= "=head$num $sec\n\n"; for my $para (@{ $podtree->{$sec} }) { if ($para eq '') { $str .= "\n"; } elsif ($para =~ /^\s+/) { $str .= $para; } else { $str .= "$para\n"; } } } $str = "=pod\n\n_LINE_ANCHOR_1\n\n$str" if $line_anchor; $str; } # convert patterns used in 00-smartlinks.to perl 5 regexes sub parse_pattern ($) { my $pat = shift; my @keys; while (1) { if ($pat =~ /\G\s*"([^"]+)"/gc || $pat =~ /\G\s*'([^']+)'/gc || $pat =~ /\G\s*(\S+)/gc) { push @keys, $1; } else { last } } my $str = join('.+?', map { my $key = quotemeta $_; $key =~ s/^\w/\\b$&/; $key =~ s/\w$/$&\\b/; $key; } @keys); $str; } # process paragraphs of the synopses: unwrap lines, strip POD tags, and etc. sub process_paragraph ($) { my $str = shift; # unwrap lines: $str =~ s/\s*\n\s*/ /g; # strip POD tags: # FIXME: obviously we need a better way to do this: $str =~ s/[LCFIB]<<<\s+(.*?)\s+>>>/$1/g; $str =~ s/[LCFIB]<<\s+(.*?)\s+>>/$1/g; $str =~ s/[LCFIB]<(.*?)>/$1/g; $str; } sub gen_html ($$$) { my ($pod, $syn_id, $cssfile) = @_; eval { require Pod::Simple::HTML }; die "error: Pod::Simple::HTML is not installed on your machine.\n" if $@; $Pod::Simple::HTML::Content_decl = q{}; $Pod::Simple::HTML::Doctype_decl = qq{\n}; my $pod2html = new Pod::Simple::HTML; $pod2html->index(1); $pod2html->html_css($cssfile); $pod2html->html_javascript(qq{}); $pod2html->force_title('S'.$syn_id); my $html; open my $in, '<', \$pod; open my $out, '>', \$html; $pod2html->parse_from_file($in, $out); # substitutes the placeholders introduced by `gen_code_snippet` # with real code snippets: $html =~ s,(?:
\s*)?\b_SMART_LINK_(\d+)\b(?:\s*
)?,$snippets[$1],sg; fix_line_anchors(\$html) if $line_anchor; add_footer(\$html); add_user_css(\$html); $html } sub fix_line_anchors { my ($html) = @_; my @lineno; # line numbers for each paragraph while ($$html =~ /\b_LINE_ANCHOR_(\d+)\b/gsm) { push @lineno, $1; } $$html =~ s{(?:\s*)?\b_LINE_ANCHOR_(\d+)\b(?:\s*
)?}{ gen_line_anchors(\@lineno) }sge; } sub gen_line_anchors { my $list = shift; my $curr = shift @$list; my $html = ''; for ($curr .. $list->[0] - 1) { $html .= qq{\n}; } $html; } sub add_footer { my ($html) = @_; $$html =~ s{