diff options
author | berkeviktor@aol.com <berkeviktor@aol.com> | 2011-02-24 04:14:30 +0100 |
---|---|---|
committer | berkeviktor@aol.com <berkeviktor@aol.com> | 2011-02-24 04:14:30 +0100 |
commit | 4a6ceffb98a0b785494f680d3776c4bfc4052f9e (patch) | |
tree | 850703c1c841ccd99f58d0b06084615aaebe782c /plugins/perl/lib | |
parent | f16af8be941b596dedac3bf4e371ee2d21f4b598 (diff) |
add xchat r1489
Diffstat (limited to 'plugins/perl/lib')
-rw-r--r-- | plugins/perl/lib/IRC.pm | 257 | ||||
-rw-r--r-- | plugins/perl/lib/Pod/Html.pm | 2399 | ||||
-rw-r--r-- | plugins/perl/lib/Xchat.pm | 506 | ||||
-rw-r--r-- | plugins/perl/lib/Xchat.pod | 1326 | ||||
-rw-r--r-- | plugins/perl/lib/Xchat/Embed.pm | 253 | ||||
-rw-r--r-- | plugins/perl/lib/Xchat/List/Network.pm | 32 | ||||
-rw-r--r-- | plugins/perl/lib/Xchat/List/Network/AutoJoin.pm | 82 | ||||
-rw-r--r-- | plugins/perl/lib/Xchat/List/Network/Entry.pm | 105 |
8 files changed, 4960 insertions, 0 deletions
diff --git a/plugins/perl/lib/IRC.pm b/plugins/perl/lib/IRC.pm new file mode 100644 index 00000000..c22a8e73 --- /dev/null +++ b/plugins/perl/lib/IRC.pm @@ -0,0 +1,257 @@ + +package IRC; +sub IRC::register { + my ($script_name, $version, $callback) = @_; + my $package = caller; + $callback = Xchat::Embed::fix_callback( $package, $callback) if $callback; + Xchat::register( $script_name, $version, undef, $callback ); +} + + +sub IRC::add_command_handler { + my ($command, $callback) = @_; + my $package = caller; + + $callback = Xchat::Embed::fix_callback( $package, $callback ); + + # starting index for word_eol array + # this is for compatibility with '' as the command + my $start_index = $command ? 1 : 0; + + Xchat::hook_command( $command, + sub { + no strict 'refs'; + return &{$callback}($_[1][$start_index]); + } + ); + return; +} + +sub IRC::add_message_handler { + my ($message, $callback) = @_; + my $package = caller; + $callback = Xchat::Embed::fix_callback( $package, $callback ); + + Xchat::hook_server( $message, + sub { + no strict 'refs'; + return &{$callback}( $_[1][0] ); + } + ); + return; +} + +sub IRC::add_print_handler { + my ($event, $callback) = @_; + my $package = caller; + $callback = Xchat::Embed::fix_callback( $package, $callback ); + Xchat::hook_print( $event, + sub { + my @word = @{$_[0]}; + no strict 'refs'; + return &{$callback}( join( ' ', @word[0..3] ), @word ); + } + ); + return; +} + +sub IRC::add_timeout_handler { + my ($timeout, $callback) = @_; + my $package = caller; + $callback = Xchat::Embed::fix_callback( $package, $callback ); + Xchat::hook_timer( $timeout, + sub { + no strict 'refs'; + &{$callback}; + return 0; + } + ); + return; +} + +sub IRC::command { + my $command = shift; + if( $command =~ m{^/} ) { + $command =~ s{^/}{}; + Xchat::command( $command ); + } else { + Xchat::command( qq[say $command] ); + } +} + +sub IRC::command_with_channel { + my ($command, $channel, $server) = @_; + my $old_ctx = Xchat::get_context; + my $ctx = Xchat::find_context( $channel, $server ); + + if( $ctx ) { + Xchat::set_context( $ctx ); + IRC::command( $command ); + Xchat::set_context( $ctx ); + } +} + +sub IRC::command_with_server { + my ($command, $server) = @_; + my $old_ctx = Xchat::get_context; + my $ctx = Xchat::find_context( undef, $server ); + + if( $ctx ) { + Xchat::set_context( $ctx ); + IRC::command( $command ); + Xchat::set_context( $ctx ); + } +} + +sub IRC::dcc_list { + my @dccs; + for my $dcc ( Xchat::get_list( 'dcc' ) ) { + push @dccs, $dcc->{nick}; + push @dccs, $dcc->{file} ? $dcc->{file} : ''; + push @dccs, @{$dcc}{qw(type status cps size)}; + push @dccs, $dcc->{type} == 0 ? $dcc->{pos} : $dcc->{resume}; + push @dccs, $dcc->{address32}; + push @dccs, $dcc->{destfile} ? $dcc->{destfile} : ''; + } + return @dccs; +} + +sub IRC::channel_list { + my @channels; + for my $channel ( Xchat::get_list( 'channels' ) ) { + push @channels, @{$channel}{qw(channel server)}, + Xchat::context_info( $channel->{context} )->{nick}; + } + return @channels; +} + +sub IRC::get_info { + my $id = shift; + my @ids = qw(version nick channel server xchatdir away network host topic); + + if( $id >= 0 && $id <= 8 && $id != 5 ) { + my $info = Xchat::get_info($ids[$id]); + return defined $info ? $info : ''; + } else { + if( $id == 5 ) { + return Xchat::get_info( 'away' ) ? 1 : 0; + } else { + return 'Error2'; + } + } +} + +sub IRC::get_prefs { + return 'Unknown variable' unless defined $_[0]; + my $result = Xchat::get_prefs(shift); + return defined $result ? $result : 'Unknown variable'; +} + +sub IRC::ignore_list { + my @ignores; + for my $ignore ( Xchat::get_list( 'ignore' ) ) { + push @ignores, $ignore->{mask}; + my $flags = $ignore->{flags}; + push @ignores, $flags & 1, $flags & 2, $flags & 4, $flags & 8, $flags & 16, + $flags & 32, ':'; + } + return @ignores; +} + +sub IRC::print { + Xchat::print( $_ ) for @_; + return; +} + +sub IRC::print_with_channel { + Xchat::print( @_ ); +} + +sub IRC::send_raw { + Xchat::commandf( qq[quote %s], shift ); +} + +sub IRC::server_list { + my @servers; + for my $channel ( Xchat::get_list( 'channels' ) ) { + push @servers, $channel->{server} if $channel->{server}; + } + return @servers; +} + +sub IRC::user_info { + my $user; + if( @_ > 0 ) { + $user = Xchat::user_info( shift ); + } else { + $user = Xchat::user_info(); + } + + my @info; + if( $user ) { + push @info, $user->{nick}; + if( $user->{host} ) { + push @info, $user->{host}; + } else { + push @info, 'FETCHING'; + } + push @info, $user->{prefix} eq '@' ? 1 : 0; + push @info, $user->{prefix} eq '+' ? 1 : 0; + } + return @info; +} + +sub IRC::user_list { + my ($channel, $server) = @_; + my $ctx = Xchat::find_context( $channel, $server ); + my $old_ctx = Xchat::get_context; + + if( $ctx ) { + Xchat::set_context( $ctx ); + my @users; + for my $user ( Xchat::get_list( 'users' ) ) { + push @users, $user->{nick}; + if( $user->{host} ) { + push @users, $user->{host}; + } else { + push @users, 'FETCHING'; + } + push @users, $user->{prefix} eq '@' ? 1 : 0; + push @users, $user->{prefix} eq '+' ? 1 : 0; + push @users, ':'; + } + Xchat::set_context( $old_ctx ); + return @users; + } else { + return; + } +} + +sub IRC::user_list_short { + my ($channel, $server) = @_; + my $ctx = Xchat::find_context( $channel, $server ); + my $old_ctx = Xchat::get_context; + + if( $ctx ) { + Xchat::set_context( $ctx ); + my @users; + for my $user ( Xchat::get_list( 'users' ) ) { + my $nick = $user->{nick}; + my $host = $user->{host} || 'FETCHING'; + push @users, $nick, $host; + } + Xchat::set_context( $old_ctx ); + return @users; + } else { + return; + } + +} + +sub IRC::add_user_list {} +sub IRC::sub_user_list {} +sub IRC::clear_user_list {} +sub IRC::notify_list {} +sub IRC::perl_script_list {} + +1 diff --git a/plugins/perl/lib/Pod/Html.pm b/plugins/perl/lib/Pod/Html.pm new file mode 100644 index 00000000..3695564b --- /dev/null +++ b/plugins/perl/lib/Pod/Html.pm @@ -0,0 +1,2399 @@ +package Pod::Html; +use strict; +require Exporter; + +use vars qw($VERSION @ISA @EXPORT @EXPORT_OK); +$VERSION = 1.08; +@ISA = qw(Exporter); +@EXPORT = qw(pod2html htmlify); +@EXPORT_OK = qw(anchorify); + +use Carp; +use Config; +use Cwd; +use File::Spec; +use File::Spec::Unix; +use Getopt::Long; + +use locale; # make \w work right in non-ASCII lands + +=head1 NAME + +Pod::Html - module to convert pod files to HTML + +=head1 SYNOPSIS + + use Pod::Html; + pod2html([options]); + +=head1 DESCRIPTION + +Converts files from pod format (see L<perlpod>) to HTML format. It +can automatically generate indexes and cross-references, and it keeps +a cache of things it knows how to cross-reference. + +=head1 FUNCTIONS + +=head2 pod2html + + pod2html("pod2html", + "--podpath=lib:ext:pod:vms", + "--podroot=/usr/src/perl", + "--htmlroot=/perl/nmanual", + "--libpods=perlfunc:perlguts:perlvar:perlrun:perlop", + "--recurse", + "--infile=foo.pod", + "--outfile=/perl/nmanual/foo.html"); + +pod2html takes the following arguments: + +=over 4 + +=item backlink + + --backlink="Back to Top" + +Adds "Back to Top" links in front of every C<head1> heading (except for +the first). By default, no backlinks are generated. + +=item cachedir + + --cachedir=name + +Creates the item and directory caches in the given directory. + +=item css + + --css=stylesheet + +Specify the URL of a cascading style sheet. Also disables all HTML/CSS +C<style> attributes that are output by default (to avoid conflicts). + +=item flush + + --flush + +Flushes the item and directory caches. + +=item header + + --header + --noheader + +Creates header and footer blocks containing the text of the C<NAME> +section. By default, no headers are generated. + +=item help + + --help + +Displays the usage message. + +=item hiddendirs + + --hiddendirs + --nohiddendirs + +Include hidden directories in the search for POD's in podpath if recurse +is set. +The default is not to traverse any directory whose name begins with C<.>. +See L</"podpath"> and L</"recurse">. + +[This option is for backward compatibility only. +It's hard to imagine that one would usefully create a module with a +name component beginning with C<.>.] + +=item htmldir + + --htmldir=name + +Sets the directory in which the resulting HTML file is placed. This +is used to generate relative links to other files. Not passing this +causes all links to be absolute, since this is the value that tells +Pod::Html the root of the documentation tree. + +=item htmlroot + + --htmlroot=name + +Sets the base URL for the HTML files. When cross-references are made, +the HTML root is prepended to the URL. + +=item index + + --index + --noindex + +Generate an index at the top of the HTML file. This is the default +behaviour. + +=item infile + + --infile=name + +Specify the pod file to convert. Input is taken from STDIN if no +infile is specified. + +=item libpods + + --libpods=name:...:name + +List of page names (eg, "perlfunc") which contain linkable C<=item>s. + +=item netscape + + --netscape + --nonetscape + +B<Deprecated>, has no effect. For backwards compatibility only. + +=item outfile + + --outfile=name + +Specify the HTML file to create. Output goes to STDOUT if no outfile +is specified. + +=item podpath + + --podpath=name:...:name + +Specify which subdirectories of the podroot contain pod files whose +HTML converted forms can be linked to in cross references. + +=item podroot + + --podroot=name + +Specify the base directory for finding library pods. + +=item quiet + + --quiet + --noquiet + +Don't display I<mostly harmless> warning messages. These messages +will be displayed by default. But this is not the same as C<verbose> +mode. + +=item recurse + + --recurse + --norecurse + +Recurse into subdirectories specified in podpath (default behaviour). + +=item title + + --title=title + +Specify the title of the resulting HTML file. + +=item verbose + + --verbose + --noverbose + +Display progress messages. By default, they won't be displayed. + +=back + +=head2 htmlify + + htmlify($heading); + +Converts a pod section specification to a suitable section specification +for HTML. Note that we keep spaces and special characters except +C<", ?> (Netscape problem) and the hyphen (writer's problem...). + +=head2 anchorify + + anchorify(@heading); + +Similar to C<htmlify()>, but turns non-alphanumerics into underscores. Note +that C<anchorify()> is not exported by default. + +=head1 ENVIRONMENT + +Uses C<$Config{pod2html}> to setup default options. + +=head1 AUTHOR + +Tom Christiansen, E<lt>tchrist@perl.comE<gt>. + +=head1 SEE ALSO + +L<perlpod> + +=head1 COPYRIGHT + +This program is distributed under the Artistic License. + +=cut + +my ($Cachedir); +my ( $Dircache, $Itemcache ); +my @Begin_Stack; +my @Libpods; +my ( $Htmlroot, $Htmldir, $Htmlfile, $Htmlfileurl ); +my ( $Podfile, @Podpath, $Podroot ); +my $Css; + +my $Recurse; +my $Quiet; +my $HiddenDirs; +my $Verbose; +my $Doindex; + +my $Backlink; +my ( $Listlevel, @Listend ); +my $After_Lpar; +use vars qw($Ignore); # need to localize it later. + +my ( %Items_Named, @Items_Seen ); +my ( $Title, $Header ); + +my $Top; +my $Paragraph; + +my %Sections; + +# Caches +my %Pages = (); # associative array used to find the location + # of pages referenced by L<> links. +my %Items = (); # associative array used to find the location + # of =item directives referenced by C<> links + +my %Local_Items; +my $Is83; + +my $Curdir = File::Spec->curdir; + +init_globals(); + +sub init_globals { + $Cachedir = "."; # The directory to which item and directory + # caches will be written. + + $Dircache = "pod2htmd.tmp"; + $Itemcache = "pod2htmi.tmp"; + + @Begin_Stack = (); # begin/end stack + + @Libpods = (); # files to search for links from C<> directives + $Htmlroot = "/"; # http-server base directory from which all + # relative paths in $podpath stem. + $Htmldir = ""; # The directory to which the html pages + # will (eventually) be written. + $Htmlfile = ""; # write to stdout by default + $Htmlfileurl = ""; # The url that other files would use to + # refer to this file. This is only used + # to make relative urls that point to + # other files. + + $Podfile = ""; # read from stdin by default + @Podpath = (); # list of directories containing library pods. + $Podroot = $Curdir; # filesystem base directory from which all + # relative paths in $podpath stem. + $Css = ''; # Cascading style sheet + $Recurse = 1; # recurse on subdirectories in $podpath. + $Quiet = 0; # not quiet by default + $Verbose = 0; # not verbose by default + $Doindex = 1; # non-zero if we should generate an index + $Backlink = ''; # text for "back to top" links + $Listlevel = 0; # current list depth + @Listend = (); # the text to use to end the list. + $After_Lpar = 0; # set to true after a par in an =item + $Ignore = 1; # whether or not to format text. we don't + # format text until we hit our first pod + # directive. + + @Items_Seen = (); # for multiples of the same item in perlfunc + %Items_Named = (); + $Header = 0; # produce block header/footer + $Title = ''; # title to give the pod(s) + $Top = 1; # true if we are at the top of the doc. used + # to prevent the first <hr /> directive. + $Paragraph = ''; # which paragraph we're processing (used + # for error messages) + %Sections = (); # sections within this page + + %Local_Items = (); + $Is83 = $^O eq 'dos'; # Is it an 8.3 filesystem? +} + +# +# clean_data: global clean-up of pod data +# +sub clean_data($) { + my ($dataref) = @_; + for my $i ( 0 .. $#{$dataref} ) { + ${$dataref}[$i] =~ s/\s+\Z//; + + # have a look for all-space lines + if ( ${$dataref}[$i] =~ /^\s+$/m and $dataref->[$i] !~ /^\s/ ) { + my @chunks = split( /^\s+$/m, ${$dataref}[$i] ); + splice( @$dataref, $i, 1, @chunks ); + } + } +} + +sub pod2html { + local (@ARGV) = @_; + local ($/); + local $_; + + init_globals(); + + $Is83 = 0 if ( defined(&Dos::UseLFN) && Dos::UseLFN() ); + + # cache of %Pages and %Items from last time we ran pod2html + + #undef $opt_help if defined $opt_help; + + # parse the command-line parameters + parse_command_line(); + + # escape the backlink argument (same goes for title but is done later...) + $Backlink = html_escape($Backlink) if defined $Backlink; + + # set some variables to their default values if necessary + local *POD; + unless ( @ARGV && $ARGV[0] ) { + $Podfile = "-" unless $Podfile; # stdin + open( POD, "<$Podfile" ) + || die "$0: cannot open $Podfile file for input: $!\n"; + } else { + $Podfile = $ARGV[0]; # XXX: might be more filenames + *POD = *ARGV; + } + $Htmlfile = "-" unless $Htmlfile; # stdout + $Htmlroot = "" if $Htmlroot eq "/"; # so we don't get a // + $Htmldir =~ s#/\z##; # so we don't get a // + if ( $Htmlroot eq '' + && defined($Htmldir) + && $Htmldir ne '' + && substr( $Htmlfile, 0, length($Htmldir) ) eq $Htmldir ) + { + + # Set the 'base' url for this file, so that we can use it + # as the location from which to calculate relative links + # to other files. If this is '', then absolute links will + # be used throughout. + $Htmlfileurl = "$Htmldir/" . substr( $Htmlfile, length($Htmldir) + 1 ); + } + + # read the pod a paragraph at a time + warn "Scanning for sections in input file(s)\n" if $Verbose; + $/ = ""; + my @poddata = <POD>; + close(POD); + + # be eol agnostic + for (@poddata) { + if (/\r/) { + if (/\r\n/) { + @poddata = map { + s/\r\n/\n/g; + /\n\n/ + ? map { "$_\n\n" } split /\n\n/ + : $_ + } @poddata; + } else { + @poddata = map { + s/\r/\n/g; + /\n\n/ + ? map { "$_\n\n" } split /\n\n/ + : $_ + } @poddata; + } + last; + } + } + + clean_data( \@poddata ); + + # scan the pod for =head[1-6] directives and build an index + my $index = scan_headings( \%Sections, @poddata ); + + unless ($index) { + warn "No headings in $Podfile\n" if $Verbose; + } + + # open the output file + open( HTML, ">$Htmlfile" ) + || die "$0: cannot open $Htmlfile file for output: $!\n"; + + # put a title in the HTML file if one wasn't specified + if ( $Title eq '' ) { + TITLE_SEARCH: { + for ( my $i = 0 ; $i < @poddata ; $i++ ) { + if ( $poddata[$i] =~ /^=head1\s*NAME\b/m ) { + for my $para ( @poddata[ $i, $i + 1 ] ) { + last TITLE_SEARCH + if ($Title) = $para =~ /(\S+\s+-+.*\S)/s; + } + } + + } + } + } + if ( !$Title and $Podfile =~ /\.pod\z/ ) { + + # probably a split pod so take first =head[12] as title + for ( my $i = 0 ; $i < @poddata ; $i++ ) { + last if ($Title) = $poddata[$i] =~ /^=head[12]\s*(.*)/; + } + warn "adopted '$Title' as title for $Podfile\n" + if $Verbose and $Title; + } + if ($Title) { + $Title =~ s/\s*\(.*\)//; + } else { + warn "$0: no title for $Podfile.\n" unless $Quiet; + $Podfile =~ /^(.*)(\.[^.\/]+)?\z/s; + $Title = ( $Podfile eq "-" ? 'No Title' : $1 ); + warn "using $Title" if $Verbose; + } + $Title = html_escape($Title); + + my $csslink = ''; + my $bodystyle = ' style="background-color: white"'; + my $tdstyle = ' style="background-color: #cccccc"'; + + if ($Css) { + $csslink = qq(\n<link rel="stylesheet" href="$Css" type="text/css" />); + $csslink =~ s,\\,/,g; + $csslink =~ s,(/.):,$1|,; + $bodystyle = ''; + $tdstyle = ''; + } + + my $block = $Header ? <<END_OF_BLOCK : ''; +<table border="0" width="100%" cellspacing="0" cellpadding="3"> +<tr><td class="block"$tdstyle valign="middle"> +<big><strong><span class="block"> $Title</span></strong></big> +</td></tr> +</table> +END_OF_BLOCK + + print HTML <<END_OF_HEAD; +<?xml version="1.0" ?> +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> +<title>$Title</title>$csslink +<meta http-equiv="content-type" content="text/html; charset=utf-8" /> +<link rev="made" href="mailto:$Config{perladmin}" /> +<style type="text/css"> + .branch { + list-style: none; + } + + li code { + padding-right: 0.5em; + } + + .example { + width: 98%; + padding: 0.5em; + float: left; + font-family: monospace; + } + + .example .line_number { + float: left; + text-align: right; + margin-right: 10px; + padding-right: 5px; + border-right: 1px solid white; + } + + .example .content { + float: left; + } + + .example .content pre { + margin: 0; + } + + td > table { + margin: 0.5em; + border-collapse: collapse; + } + + td > table td { + border: 1px solid black; + } + + .synComment { + color: rgb(135,206,235); + } + .synPreProc { + color: rgb(205,92,92); + } + .synError { + } + .synConstant { + color: #ffa0a0; + } + .synSpecial { + color: rgb(255,222,173); + } + .synIgnore { + color: rgb(102,102,102); + } + .synNormal { + color: rgb(255,255,255); + background-color: rgb(51,51,51); + } + .synType { + color: rgb(189,183,107); + } + .synIdentifier { + color: rgb(152,251,152); + } + .synTodo { + color: rgb(255,69,0); + background-color: rgb(238,238,0); + } + .synStatement { + color: rgb(240,230,140); + } + +</style> +</head> + +<body$bodystyle> +$block +END_OF_HEAD + + # load/reload/validate/cache %Pages and %Items + get_cache( $Dircache, $Itemcache, \@Podpath, $Podroot, $Recurse ); + + # scan the pod for =item directives + scan_items( \%Local_Items, "", @poddata ); + + # put an index at the top of the file. note, if $Doindex is 0 we + # still generate an index, but surround it with an html comment. + # that way some other program can extract it if desired. + $index =~ s/--+/-/g; + + my $hr = ( $Doindex and $index ) ? qq(<hr />) : ""; + + unless ($Doindex) { + $index = qq(<!--\n$index\n-->\n); + } + + print HTML << "END_OF_INDEX"; + +<!-- INDEX BEGIN --> +<div> +<p><a name=\"__index__\"></a></p> +$index +$hr +</div> +<!-- INDEX END --> + +END_OF_INDEX + + # now convert this file + my $after_item; # set to true after an =item + my $need_dd = 0; + warn "Converting input file $Podfile\n" if $Verbose; + foreach my $i ( 0 .. $#poddata ) { + $_ = $poddata[$i]; + $Paragraph = $i + 1; + if (/^(=.*)/s) { # is it a pod directive? + $Ignore = 0; + $after_item = 0; + $need_dd = 0; + $_ = $1; + if (/^=begin\s+(\S+)\s*(.*)/si) { # =begin + process_begin( $1, $2 ); + } elsif (/^=end\s+(\S+)\s*(.*)/si) { # =end + process_end( $1, $2 ); + } elsif (/^=cut/) { # =cut + process_cut(); + } elsif (/^=pod/) { # =pod + process_pod(); + } else { + next if @Begin_Stack && $Begin_Stack[-1] ne 'html'; + + if (/^=(head[1-6])\s+(.*\S)/s) { # =head[1-6] heading + process_head( $1, $2, $Doindex && $index ); + } elsif (/^=item\s*(.*\S)?/sm) { # =item text + $need_dd = process_item($1); + $after_item = 1; + } elsif (/^=over\s*(.*)/) { # =over N + process_over(); + } elsif (/^=back/) { # =back + process_back($need_dd); + } elsif (/^=for\s+(\S+)\s*(.*)/si) { # =for + process_for( $1, $2 ); + } else { + /^=(\S*)\s*/; + warn "$0: $Podfile: unknown pod directive '$1' in " + . "paragraph $Paragraph. ignoring.\n" + unless $Quiet; + } + } + $Top = 0; + } else { + next if $Ignore; + next if @Begin_Stack && $Begin_Stack[-1] ne 'html'; + print HTML and next if @Begin_Stack && $Begin_Stack[-1] eq 'html'; + print HTML "<dd>\n" if $need_dd; + my $text = $_; + if ( $text =~ /\A\s+/ ) { + process_pre( \$text ); + print HTML "<pre>\n$text</pre>\n"; + + } else { + process_text( \$text ); + + # experimental: check for a paragraph where all lines + # have some ...\t...\t...\n pattern + if ( $text =~ /\t/ ) { + my @lines = split( "\n", $text ); + if ( @lines > 1 ) { + my $all = 2; + foreach my $line (@lines) { + if ( $line =~ /\S/ && $line !~ /\t/ ) { + $all--; + last if $all == 0; + } + } + if ( $all > 0 ) { + $text =~ s/\t+/<td>/g; + $text =~ s/^/<tr><td>/gm; + $text = + '<table cellspacing="0" cellpadding="0">' + . $text + . '</table>'; + } + } + } + ## end of experimental + + if ($after_item) { + $After_Lpar = 1; + } + print HTML "<p>$text</p>\n"; + } + print HTML "</dd>\n" if $need_dd; + $after_item = 0; + } + } + + # finish off any pending directives + finish_list(); + + # link to page index + print HTML "<p><a href=\"#__index__\"><small>$Backlink</small></a></p>\n" + if $Doindex + and $index + and $Backlink; + + print HTML <<END_OF_TAIL; +$block +</body> + +</html> +END_OF_TAIL + + # close the html file + close(HTML); + + warn "Finished\n" if $Verbose; +} + +############################################################################## + +sub usage { + my $podfile = shift; + warn "$0: $podfile: @_\n" if @_; + die <<END_OF_USAGE; +Usage: $0 --help --htmlroot=<name> --infile=<name> --outfile=<name> + --podpath=<name>:...:<name> --podroot=<name> + --libpods=<name>:...:<name> --recurse --verbose --index + --netscape --norecurse --noindex --cachedir=<name> + + --backlink - set text for "back to top" links (default: none). + --cachedir - directory for the item and directory cache files. + --css - stylesheet URL + --flush - flushes the item and directory caches. + --[no]header - produce block header/footer (default is no headers). + --help - prints this message. + --hiddendirs - search hidden directories in podpath + --htmldir - directory for resulting HTML files. + --htmlroot - http-server base directory from which all relative paths + in podpath stem (default is /). + --[no]index - generate an index at the top of the resulting html + (default behaviour). + --infile - filename for the pod to convert (input taken from stdin + by default). + --libpods - colon-separated list of pages to search for =item pod + directives in as targets of C<> and implicit links (empty + by default). note, these are not filenames, but rather + page names like those that appear in L<> links. + --outfile - filename for the resulting html file (output sent to + stdout by default). + --podpath - colon-separated list of directories containing library + pods (empty by default). + --podroot - filesystem base directory from which all relative paths + in podpath stem (default is .). + --[no]quiet - suppress some benign warning messages (default is off). + --[no]recurse - recurse on those subdirectories listed in podpath + (default behaviour). + --title - title that will appear in resulting html file. + --[no]verbose - self-explanatory (off by default). + --[no]netscape - deprecated, has no effect. for backwards compatibility only. + +END_OF_USAGE + +} + +sub parse_command_line { + my ( + $opt_backlink, $opt_cachedir, $opt_css, $opt_flush, + $opt_header, $opt_help, $opt_htmldir, $opt_htmlroot, + $opt_index, $opt_infile, $opt_libpods, $opt_netscape, + $opt_outfile, $opt_podpath, $opt_podroot, $opt_quiet, + $opt_recurse, $opt_title, $opt_verbose, $opt_hiddendirs + ); + + unshift @ARGV, split ' ', $Config{pod2html} if $Config{pod2html}; + my $result = GetOptions( + 'backlink=s' => \$opt_backlink, + 'cachedir=s' => \$opt_cachedir, + 'css=s' => \$opt_css, + 'flush' => \$opt_flush, + 'header!' => \$opt_header, + 'help' => \$opt_help, + 'hiddendirs!' => \$opt_hiddendirs, + 'htmldir=s' => \$opt_htmldir, + 'htmlroot=s' => \$opt_htmlroot, + 'index!' => \$opt_index, + 'infile=s' => \$opt_infile, + 'libpods=s' => \$opt_libpods, + 'netscape!' => \$opt_netscape, + 'outfile=s' => \$opt_outfile, + 'podpath=s' => \$opt_podpath, + 'podroot=s' => \$opt_podroot, + 'quiet!' => \$opt_quiet, + 'recurse!' => \$opt_recurse, + 'title=s' => \$opt_title, + 'verbose!' => \$opt_verbose, + ); + usage( "-", "invalid parameters" ) if not $result; + + usage("-") if defined $opt_help; # see if the user asked for help + $opt_help = ""; # just to make -w shut-up. + + @Podpath = split( ":", $opt_podpath ) if defined $opt_podpath; + @Libpods = split( ":", $opt_libpods ) if defined $opt_libpods; + + $Backlink = $opt_backlink if defined $opt_backlink; + $Cachedir = $opt_cachedir if defined $opt_cachedir; + $Css = $opt_css if defined $opt_css; + $Header = $opt_header if defined $opt_header; + $Htmldir = $opt_htmldir if defined $opt_htmldir; + $Htmlroot = $opt_htmlroot if defined $opt_htmlroot; + $Doindex = $opt_index if defined $opt_index; + $Podfile = $opt_infile if defined $opt_infile; + $HiddenDirs = $opt_hiddendirs if defined $opt_hiddendirs; + $Htmlfile = $opt_outfile if defined $opt_outfile; + $Podroot = $opt_podroot if defined $opt_podroot; + $Quiet = $opt_quiet if defined $opt_quiet; + $Recurse = $opt_recurse if defined $opt_recurse; + $Title = $opt_title if defined $opt_title; + $Verbose = $opt_verbose if defined $opt_verbose; + + warn "Flushing item and directory caches\n" + if $opt_verbose && defined $opt_flush; + $Dircache = "$Cachedir/pod2htmd.tmp"; + $Itemcache = "$Cachedir/pod2htmi.tmp"; + if ( defined $opt_flush ) { + 1 while unlink( $Dircache, $Itemcache ); + } +} + +my $Saved_Cache_Key; + +sub get_cache { + my ( $dircache, $itemcache, $podpath, $podroot, $recurse ) = @_; + my @cache_key_args = @_; + + # A first-level cache: + # Don't bother reading the cache files if they still apply + # and haven't changed since we last read them. + + my $this_cache_key = cache_key(@cache_key_args); + + return if $Saved_Cache_Key and $this_cache_key eq $Saved_Cache_Key; + + # load the cache of %Pages and %Items if possible. $tests will be + # non-zero if successful. + my $tests = 0; + if ( -f $dircache && -f $itemcache ) { + warn "scanning for item cache\n" if $Verbose; + $tests = load_cache( $dircache, $itemcache, $podpath, $podroot ); + } + + # if we didn't succeed in loading the cache then we must (re)build + # %Pages and %Items. + if ( !$tests ) { + warn "scanning directories in pod-path\n" if $Verbose; + scan_podpath( $podroot, $recurse, 0 ); + } + $Saved_Cache_Key = cache_key(@cache_key_args); +} + +sub cache_key { + my ( $dircache, $itemcache, $podpath, $podroot, $recurse ) = @_; + return join( '!', + $dircache, $itemcache, $recurse, @$podpath, $podroot, stat($dircache), + stat($itemcache) ); +} + +# +# load_cache - tries to find if the caches stored in $dircache and $itemcache +# are valid caches of %Pages and %Items. if they are valid then it loads +# them and returns a non-zero value. +# +sub load_cache { + my ( $dircache, $itemcache, $podpath, $podroot ) = @_; + my ($tests); + local $_; + + $tests = 0; + + open( CACHE, "<$itemcache" ) + || die "$0: error opening $itemcache for reading: $!\n"; + $/ = "\n"; + + # is it the same podpath? + $_ = <CACHE>; + chomp($_); + $tests++ if ( join( ":", @$podpath ) eq $_ ); + + # is it the same podroot? + $_ = <CACHE>; + chomp($_); + $tests++ if ( $podroot eq $_ ); + + # load the cache if its good + if ( $tests != 2 ) { + close(CACHE); + return 0; + } + + warn "loading item cache\n" if $Verbose; + while (<CACHE>) { + /(.*?) (.*)$/; + $Items{$1} = $2; + } + close(CACHE); + + warn "scanning for directory cache\n" if $Verbose; + open( CACHE, "<$dircache" ) + || die "$0: error opening $dircache for reading: $!\n"; + $/ = "\n"; + $tests = 0; + + # is it the same podpath? + $_ = <CACHE>; + chomp($_); + $tests++ if ( join( ":", @$podpath ) eq $_ ); + + # is it the same podroot? + $_ = <CACHE>; + chomp($_); + $tests++ if ( $podroot eq $_ ); + + # load the cache if its good + if ( $tests != 2 ) { + close(CACHE); + return 0; + } + + warn "loading directory cache\n" if $Verbose; + while (<CACHE>) { + /(.*?) (.*)$/; + $Pages{$1} = $2; + } + + close(CACHE); + + return 1; +} + +# +# scan_podpath - scans the directories specified in @podpath for directories, +# .pod files, and .pm files. it also scans the pod files specified in +# @Libpods for =item directives. +# +sub scan_podpath { + my ( $podroot, $recurse, $append ) = @_; + my ( $pwd, $dir ); + my ( $libpod, $dirname, $pod, @files, @poddata ); + + unless ($append) { + %Items = (); + %Pages = (); + } + + # scan each directory listed in @Podpath + $pwd = getcwd(); + chdir($podroot) + || die "$0: error changing to directory $podroot: $!\n"; + foreach $dir (@Podpath) { + scan_dir( $dir, $recurse ); + } + + # scan the pods listed in @Libpods for =item directives + foreach $libpod (@Libpods) { + + # if the page isn't defined then we won't know where to find it + # on the system. + next unless defined $Pages{$libpod} && $Pages{$libpod}; + + # if there is a directory then use the .pod and .pm files within it. + # NOTE: Only finds the first so-named directory in the tree. + # if ($Pages{$libpod} =~ /([^:]*[^(\.pod|\.pm)]):/) { + if ( $Pages{$libpod} =~ /([^:]*(?<!\.pod)(?<!\.pm)):/ ) { + + # find all the .pod and .pm files within the directory + $dirname = $1; + opendir( DIR, $dirname ) + || die "$0: error opening directory $dirname: $!\n"; + @files = grep( /(\.pod|\.pm)\z/ && !-d $_, readdir(DIR) ); + closedir(DIR); + + # scan each .pod and .pm file for =item directives + foreach $pod (@files) { + open( POD, "<$dirname/$pod" ) + || die "$0: error opening $dirname/$pod for input: $!\n"; + @poddata = <POD>; + close(POD); + clean_data( \@poddata ); + + scan_items( \%Items, "$dirname/$pod", @poddata ); + } + + # use the names of files as =item directives too. +### Don't think this should be done this way - confuses issues.(WL) +### foreach $pod (@files) { +### $pod =~ /^(.*)(\.pod|\.pm)$/; +### $Items{$1} = "$dirname/$1.html" if $1; +### } + } elsif ( $Pages{$libpod} =~ /([^:]*\.pod):/ + || $Pages{$libpod} =~ /([^:]*\.pm):/ ) + { + + # scan the .pod or .pm file for =item directives + $pod = $1; + open( POD, "<$pod" ) + || die "$0: error opening $pod for input: $!\n"; + @poddata = <POD>; + close(POD); + clean_data( \@poddata ); + + scan_items( \%Items, "$pod", @poddata ); + } else { + warn "$0: shouldn't be here (line " . __LINE__ . "\n" unless $Quiet; + } + } + @poddata = (); # clean-up a bit + + chdir($pwd) + || die "$0: error changing to directory $pwd: $!\n"; + + # cache the item list for later use + warn "caching items for later use\n" if $Verbose; + open( CACHE, ">$Itemcache" ) + || die "$0: error open $Itemcache for writing: $!\n"; + + print CACHE join( ":", @Podpath ) . "\n$podroot\n"; + foreach my $key ( keys %Items ) { + print CACHE "$key $Items{$key}\n"; + } + + close(CACHE); + + # cache the directory list for later use + warn "caching directories for later use\n" if $Verbose; + open( CACHE, ">$Dircache" ) + || die "$0: error open $Dircache for writing: $!\n"; + + print CACHE join( ":", @Podpath ) . "\n$podroot\n"; + foreach my $key ( keys %Pages ) { + print CACHE "$key $Pages{$key}\n"; + } + + close(CACHE); +} + +# +# scan_dir - scans the directory specified in $dir for subdirectories, .pod +# files, and .pm files. notes those that it finds. this information will +# be used later in order to figure out where the pages specified in L<> +# links are on the filesystem. +# +sub scan_dir { + my ( $dir, $recurse ) = @_; + my ( $t, @subdirs, @pods, $pod, $dirname, @dirs ); + local $_; + + @subdirs = (); + @pods = (); + + opendir( DIR, $dir ) + || die "$0: error opening directory $dir: $!\n"; + while ( defined( $_ = readdir(DIR) ) ) { + if ( -d "$dir/$_" + && $_ ne "." + && $_ ne ".." + && ( $HiddenDirs || !/^\./ ) ) + { # directory + $Pages{$_} = "" unless defined $Pages{$_}; + $Pages{$_} .= "$dir/$_:"; + push( @subdirs, $_ ); + } elsif (/\.pod\z/) { # .pod + s/\.pod\z//; + $Pages{$_} = "" unless defined $Pages{$_}; + $Pages{$_} .= "$dir/$_.pod:"; + push( @pods, "$dir/$_.pod" ); + } elsif (/\.html\z/) { # .html + s/\.html\z//; + $Pages{$_} = "" unless defined $Pages{$_}; + $Pages{$_} .= "$dir/$_.pod:"; + } elsif (/\.pm\z/) { # .pm + s/\.pm\z//; + $Pages{$_} = "" unless defined $Pages{$_}; + $Pages{$_} .= "$dir/$_.pm:"; + push( @pods, "$dir/$_.pm" ); + } elsif ( -T "$dir/$_" ) { # script(?) + local *F; + if ( open( F, "$dir/$_" ) ) { + my $line; + while ( defined( $line = <F> ) ) { + if ( $line =~ /^=(?:pod|head1)/ ) { + $Pages{$_} = "" unless defined $Pages{$_}; + $Pages{$_} .= "$dir/$_.pod:"; + last; + } + } + close(F); + } + } + } + closedir(DIR); + + # recurse on the subdirectories if necessary + if ($recurse) { + foreach my $subdir (@subdirs) { + scan_dir( "$dir/$subdir", $recurse ); + } + } +} + +# +# scan_headings - scan a pod file for head[1-6] tags, note the tags, and +# build an index. +# +sub scan_headings { + my ( $sections, @data ) = @_; + my ( $tag, $which_head, $otitle, $listdepth, $index ); + + local $Ignore = 0; + + $listdepth = 0; + $index = ""; + + # scan for =head directives, note their name, and build an index + # pointing to each of them. + foreach my $line (@data) { + if ( $line =~ /^=(head)([1-6])\s+(.*)/ ) { + ( $tag, $which_head, $otitle ) = ( $1, $2, $3 ); + + my $title = depod($otitle); + my $name = anchorify($title); + $$sections{$name} = 1; + $title = process_text( \$otitle ); + + while ( $which_head != $listdepth ) { + if ( $which_head > $listdepth ) { + $index .= "\n" + . ( "\t" x ($listdepth) ) + . ( $listdepth > 0 ? qq{<li class="branch">\n} + . "\t"x($listdepth + 1): "" ) + . "<ul>"; + $listdepth++; + } elsif ( $which_head < $listdepth ) { + $listdepth--; + $index .= "\n" + . ( "\t" x $listdepth ) + . ( $listdepth > 0 ? "\t" : "" ) + . "</ul>" + . ( $listdepth >= 0 ? "\n" . ("\t"x$listdepth) + . "</li>" : "" ) + . "\n"; + } + } + + $index .= "\n" + . ( "\t" x $listdepth ) . "<li>" + . "<a href=\"#" + . $name . "\">" + . $title + . "</a></li>"; + } + } + + # finish off the lists + while ( $listdepth-- ) { + $index .= "\n" . ( "\t" x $listdepth ) + . ($listdepth > 0 ? "\t" : "") + ."</ul>\n" + . ($listdepth > 0 ? ("\t" x $listdepth) . "</li>" : "" ); + } + + # get rid of bogus lists + $index =~ s,\t*<ul>\s*</ul>\n,,g; + + return $index; +} + +# +# scan_items - scans the pod specified by $pod for =item directives. we +# will use this information later on in resolving C<> links. +# +sub scan_items { + my ( $itemref, $pod, @poddata ) = @_; + my ( $i, $item ); + local $_; + + $pod =~ s/\.pod\z//; + $pod .= ".html" if $pod; + + foreach $i ( 0 .. $#poddata ) { + my $txt = depod( $poddata[$i] ); + + # figure out what kind of item it is. + # Build string for referencing this item. + if ( $txt =~ /\A=item\s+\*\s*(.*)\Z/s ) { # bullet + next unless $1; + $item = $1; + } elsif ( $txt =~ /\A=item\s+(?>\d+\.?)\s*(.*)\Z/s ) { # numbered list + $item = $1; + } elsif ( $txt =~ /\A=item\s+(.*)\Z/s ) { # plain item + $item = $1; + } else { + next; + } + my $fid = fragment_id($item); + $$itemref{$fid} = "$pod" if $fid; + } +} + +# +# process_head - convert a pod head[1-6] tag and convert it to HTML format. +# +sub process_head { + my ( $tag, $heading, $hasindex ) = @_; + + # figure out the level of the =head + $tag =~ /head([1-6])/; + my $level = $1; + + if ($Listlevel) { + warn +"$0: $Podfile: unterminated list at =head in paragraph $Paragraph. ignoring.\n" + unless $Quiet; + while ($Listlevel) { + process_back(); + } + } + + print HTML "<p>\n"; + if ( $level == 1 && !$Top ) { + print HTML "<a href=\"#__index__\"><small>$Backlink</small></a>\n" + if $hasindex and $Backlink; + print HTML "</p>\n<hr />\n"; + } else { + print HTML "</p>\n"; + } + + my $name = anchorify( depod($heading) ); + my $convert = process_text( \$heading ); + $convert =~ s{</?a[^>]+>}{}g; + print HTML "<h$level><a name=\"$name\" />$convert</h$level>\n"; +} + +# +# emit_item_tag - print an =item's text +# Note: The global $EmittedItem is used for inhibiting self-references. +# +my $EmittedItem; + +sub emit_item_tag($$$) { + my ( $otext, $text, $compact ) = @_; + my $item = fragment_id( depod($text), -generate ); + Carp::confess( "Undefined fragment '$text' (" + . depod($text) + . ") from fragment_id() in emit_item_tag() in $Podfile" ) + if !defined $item; + $EmittedItem = $item; + ### print STDERR "emit_item_tag=$item ($text)\n"; + + print HTML '<strong>'; + if ( $Items_Named{$item}++ ) { + print HTML process_text( \$otext ); + } else { + my $name = $item; + $name = anchorify($name); + print HTML +#qq{<a name="$name" class="item">}, + process_text( \$otext ), + # '</a>' + ; + } + print HTML "</strong>\n"; + undef($EmittedItem); +} + +sub emit_li { + my ($tag) = @_; + if ( $Items_Seen[$Listlevel]++ == 0 ) { + push( @Listend, "</$tag>" ); + print HTML "<$tag>\n"; + } + my $emitted = $tag eq 'dl' ? 'dt' : 'li'; + print HTML "<$emitted>"; + return $emitted; +} + +# +# process_item - convert a pod item tag and convert it to HTML format. +# +sub process_item { + my ($otext) = @_; + my $need_dd = 0; # set to 1 if we need a <dd></dd> after an item + + # lots of documents start a list without doing an =over. this is + # bad! but, the proper thing to do seems to be to just assume + # they did do an =over. so warn them once and then continue. + if ( $Listlevel == 0 ) { + warn +"$0: $Podfile: unexpected =item directive in paragraph $Paragraph. ignoring.\n" + unless $Quiet; + process_over(); + } + + # formatting: insert a paragraph if preceding item has >1 paragraph + if ($After_Lpar) { + print HTML $need_dd ? "</dd>\n" : "</li>\n" if $After_Lpar; + $After_Lpar = 0; + } + + # remove formatting instructions from the text + my $text = depod($otext); + + my $emitted; # the tag actually emitted, used for closing + + # all the list variants: + if ( $text =~ /\A\*/ ) { # bullet + $emitted = emit_li('ul'); + if ( $text =~ /\A\*\s+(\S.*)\Z/s ) { # with additional text + my $tag = $1; + $otext =~ s/\A\*\s+//; + emit_item_tag( $otext, $tag, 1 ); + } + print HTML "</li>" + } elsif ( $text =~ /\A\d+/ ) { # numbered list + $emitted = emit_li('ol'); + if ( $text =~ /\A(?>\d+\.?)\s*(\S.*)\Z/s ) { # with additional text + my $tag = $1; + $otext =~ s/\A\d+\.?\s*//; + emit_item_tag( $otext, $tag, 1 ); + } + print HTML "</li>"; + } else { # definition list + $emitted = emit_li('dl'); + if ( $text =~ /\A(.+)\Z/s ) { # should have text + emit_item_tag( $otext, $text, 1 ); + } + $need_dd = 1; + } + print HTML "\n"; + return $need_dd; +} + +# +# process_over - process a pod over tag and start a corresponding HTML list. +# +sub process_over { + + # start a new list + $Listlevel++; + push( @Items_Seen, 0 ); + $After_Lpar = 0; +} + +# +# process_back - process a pod back tag and convert it to HTML format. +# +sub process_back { + my $need_dd = shift; + if ( $Listlevel == 0 ) { + warn +"$0: $Podfile: unexpected =back directive in paragraph $Paragraph. ignoring.\n" + unless $Quiet; + return; + } + + # close off the list. note, I check to see if $Listend[$Listlevel] is + # defined because an =item directive may have never appeared and thus + # $Listend[$Listlevel] may have never been initialized. + $Listlevel--; + if ( defined $Listend[$Listlevel] ) { + print HTML $need_dd ? "</dd>\n" : "</li>\n" if $After_Lpar; + print HTML $Listend[$Listlevel]; + print HTML "\n"; + pop(@Listend); + } + $After_Lpar = 0; + + # clean up item count + pop(@Items_Seen); +} + +# +# process_cut - process a pod cut tag, thus start ignoring pod directives. +# +sub process_cut { + $Ignore = 1; +} + +# +# process_pod - process a pod tag, thus stop ignoring pod directives +# until we see a corresponding cut. +# +sub process_pod { + + # no need to set $Ignore to 0 cause the main loop did it +} + +# +# process_for - process a =for pod tag. if it's for html, spit +# it out verbatim, if illustration, center it, otherwise ignore it. +# +sub process_for { + my ( $whom, $text ) = @_; + if ( $whom =~ /^(pod2)?html$/i ) { + print HTML $text; + } elsif ( $whom =~ /^illustration$/i ) { + 1 while chomp $text; + for my $ext (qw[.png .gif .jpeg .jpg .tga .pcl .bmp]) { + $text .= $ext, last if -r "$text$ext"; + } + print HTML +qq{<p align="center"><img src="$text" alt="$text illustration" /></p>}; + } +} + +# +# process_begin - process a =begin pod tag. this pushes +# whom we're beginning on the begin stack. if there's a +# begin stack, we only print if it us. +# +sub process_begin { + my ( $whom, $text ) = @_; + $whom = lc($whom); + push( @Begin_Stack, $whom ); + if ( $whom =~ /^(pod2)?html$/ ) { + print HTML $text if $text; + } +} + +# +# process_end - process a =end pod tag. pop the +# begin stack. die if we're mismatched. +# +sub process_end { + my ( $whom, $text ) = @_; + $whom = lc($whom); + if ( !defined $Begin_Stack[-1] or $Begin_Stack[-1] ne $whom ) { + Carp::confess( + "Unmatched begin/end at chunk $Paragraph in pod $Podfile\n"); + } + pop(@Begin_Stack); +} + +# +# process_pre - indented paragraph, made into <pre></pre> +# +sub process_pre { + my ($text) = @_; + my ($rest); + return if $Ignore; + + $rest = $$text; + + # insert spaces in place of tabs + $rest =~ s#(.+)# + my $line = $1; + 1 while $line =~ s/(\t+)/' ' x ((length($1) * 8) - $-[0] % 8)/e; + $line; + #eg; + + # convert some special chars to HTML escapes + $rest = html_escape($rest); + + # try and create links for all occurrences of perl.* within + # the preformatted text. + $rest =~ s{ + (\s*)(perl\w+) + }{ + if ( defined $Pages{$2} ){ # is a link + qq($1<a href="$Htmlroot/$Pages{$2}">$2</a>); + } elsif (defined $Pages{dosify($2)}) { # is a link + qq($1<a href="$Htmlroot/$Pages{dosify($2)}">$2</a>); + } else { + "$1$2"; + } + }xeg; + $rest =~ s{ + (<a\ href="?) ([^>:]*:)? ([^>:]*) \.pod: ([^>:]*:)? + }{ + my $url ; + if ( $Htmlfileurl ne '' ){ + # Here, we take advantage of the knowledge + # that $Htmlfileurl ne '' implies $Htmlroot eq ''. + # Since $Htmlroot eq '', we need to prepend $Htmldir + # on the fron of the link to get the absolute path + # of the link's target. We check for a leading '/' + # to avoid corrupting links that are #, file:, etc. + my $old_url = $3 ; + $old_url = "$Htmldir$old_url" if $old_url =~ m{^\/}; + $url = relativize_url( "$old_url.html", $Htmlfileurl ); + } else { + $url = "$3.html" ; + } + "$1$url" ; + }xeg; + + # Look for embedded URLs and make them into links. We don't + # relativize them since they are best left as the author intended. + + my $urls = '(' . join( + '|', qw{ + http + telnet + mailto + news + gopher + file + wais + ftp + } + ) . ')'; + + my $ltrs = '\w'; + my $gunk = '/#~:.?+=&%@!\-'; + my $punc = '.:!?\-;'; + my $any = "${ltrs}${gunk}${punc}"; + + $rest =~ s{ + \b # start at word boundary + ( # begin $1 { + $urls : # need resource and a colon + (?!:) # Ignore File::, among others. + [$any] +? # followed by one or more of any valid + # character, but be conservative and + # take only what you need to.... + ) # end $1 } + (?= + " > # maybe pre-quoted '<a href="...">' + | # or: + [$punc]* # 0 or more punctuation + (?: # followed + [^$any] # by a non-url char + | # or + $ # end of the string + ) # + | # or else + $ # then end of the string + ) + }{<a href="$1">$1</a>}igox; + + # text should be as it is (verbatim) + $$text = $rest; +} + +# +# pure text processing +# +# pure_text/inIS_text: differ with respect to automatic C<> recognition. +# we don't want this to happen within IS +# +sub pure_text($) { + my $text = shift(); + process_puretext( $text, 1 ); +} + +sub inIS_text($) { + my $text = shift(); + process_puretext( $text, 0 ); +} + +# +# process_puretext - process pure text (without pod-escapes) converting +# double-quotes and handling implicit C<> links. +# +sub process_puretext { + my ( $text, $notinIS ) = @_; + + ## Guessing at func() or [\$\@%&]*var references in plain text is destined + ## to produce some strange looking ref's. uncomment to disable: + ## $notinIS = 0; + + my ( @words, $lead, $trail ); + + # keep track of leading and trailing white-space + $lead = ( $text =~ s/\A(\s+)//s ? $1 : "" ); + $trail = ( $text =~ s/(\s+)\Z//s ? $1 : "" ); + + # split at space/non-space boundaries + @words = split( /(?<=\s)(?=\S)|(?<=\S)(?=\s)/, $text ); + + # process each word individually + foreach my $word (@words) { + + # skip space runs + next if $word =~ /^\s*$/; + + # see if we can infer a link or a function call + # + # NOTE: This is a word based search, it won't automatically + # mark "substr($var, 1, 2)" because the 1st word would be "substr($var" + # User has to enclose those with proper C<> + + if ( + $notinIS + && $word =~ m/ + ^([a-z_]{2,}) # The function name + \( + ([0-9][a-z]* # Manual page(1) or page(1M) + |[^)]*[\$\@\%][^)]+ # ($foo), (1, @foo), (%hash) + | # () + ) + \) + ([.,;]?)$ # a possible punctuation follows + /xi + ) + { + + # has parenthesis so should have been a C<> ref + ## try for a pagename (perlXXX(1))? + my ( $func, $args, $rest ) = ( $1, $2, $3 || '' ); + if ( $args =~ /^\d+$/ ) { + my $url = page_sect( $word, '' ); + if ( defined $url ) { + $word = +qq(<a href="$url" class="man">the $word manpage</a>$rest); + next; + } + } + ## try function name for a link, append tt'ed argument list + $word = emit_C( $func, '', "($args)" ) . $rest; + +#### disabled. either all (including $\W, $\w+{.*} etc.) or nothing. +## } elsif( $notinIS && $word =~ /^[\$\@%&*]+\w+$/) { +## # perl variables, should be a C<> ref +## $word = emit_C( $word ); + + } elsif ( $word =~ m,^\w+://\w, ) { + + # looks like a URL + # Don't relativize it: leave it as the author intended + $word = qq(<a href="$word">$word</a>); + } elsif ( $word =~ /[\w.-]+\@[\w-]+\.\w/ ) { + + # looks like an e-mail address + my ( $w1, $w2, $w3 ) = ( "", $word, "" ); + ( $w1, $w2, $w3 ) = ( "(", $1, ")$2" ) if $word =~ /^\((.*?)\)(,?)/; + ( $w1, $w2, $w3 ) = ( "<", $1, ">$2" ) + if $word =~ /^<(.*?)>(,?)/; + $word = qq($w1<a href="mailto:$w2">$w2</a>$w3); + } else { + $word = html_escape($word) if $word =~ /["&<>]/; + } + } + + # put everything back together + return $lead . join( '', @words ) . $trail; +} + +# +# process_text - handles plaintext that appears in the input pod file. +# there may be pod commands embedded within the text so those must be +# converted to html commands. +# + +sub process_text1($$;$$); +sub pattern ($) { $_[0] ? '\s+' . ( '>' x ( $_[0] + 1 ) ) : '>' } +sub closing ($) { local ($_) = shift; ( defined && s/\s+\z// ) ? length : 0 } + +sub process_text { + return if $Ignore; + my ($tref) = @_; + my $res = process_text1( 0, $tref ); + $res =~ s/\s+$//s; + $$tref = $res; +} + +sub process_text_rfc_links { + my $text = shift; + + # For every "RFCnnnn" or "RFC nnn", link it to the authoritative + # ource. Do not use the /i modifier here. Require "RFC" to be written in + # in capital letters. + + $text =~ s{ + (?<=[^<>[:alpha:]]) # Make sure this is not an URL already + (RFC\s*([0-9]{1,5}))(?![0-9]) # max 5 digits + } + {<a href="http://www.ietf.org/rfc/rfc$2.txt" class="rfc">$1</a>}gx; + + $text; +} + +sub process_text1($$;$$) { + my ( $lev, $rstr, $func, $closing ) = @_; + my $res = ''; + + unless ( defined $func ) { + $func = ''; + $lev++; + } + + if ( $func eq 'B' ) { + + # B<text> - boldface + $res = '<strong>' . process_text1( $lev, $rstr ) . '</strong>'; + + } elsif ( $func eq 'C' ) { + + # C<code> - can be a ref or <code></code> + # need to extract text + my $par = go_ahead( $rstr, 'C', $closing ); + + ## clean-up of the link target + my $text = depod($par); + + ### my $x = $par =~ /[BI]</ ? 'yes' : 'no' ; + ### print STDERR "-->call emit_C($par) lev=$lev, par with BI=$x\n"; + + $res = emit_C( $text, $lev > 1 || ( $par =~ /[BI]</ ) ); + + } elsif ( $func eq 'E' ) { + + # E<x> - convert to character + $$rstr =~ s/^([^>]*)>//; + my $escape = $1; + $escape =~ s/^(\d+|X[\dA-F]+)$/#$1/i; + $res = "&$escape;"; + + } elsif ( $func eq 'F' ) { + + # F<filename> - italicize + $res = '<em class="file">' . process_text1( $lev, $rstr ) . '</em>'; + + } elsif ( $func eq 'I' ) { + + # I<text> - italicize + $res = '<em>' . process_text1( $lev, $rstr ) . '</em>'; + + } elsif ( $func eq 'L' ) { + + # L<link> - link + ## L<text|cross-ref> => produce text, use cross-ref for linking + ## L<cross-ref> => make text from cross-ref + ## need to extract text + my $par = go_ahead( $rstr, 'L', $closing ); + + # some L<>'s that shouldn't be: + # a) full-blown URL's are emitted as-is + if ( $par =~ m{^\w+://}s ) { + return make_URL_href($par); + } + + # b) C<...> is stripped and treated as C<> + if ( $par =~ /^C<(.*)>$/ ) { + my $text = depod($1); + return emit_C( $text, $lev > 1 || ( $par =~ /[BI]</ ) ); + } + + # analyze the contents + $par =~ s/\n/ /g; # undo word-wrapped tags + my $opar = $par; + my $linktext; + if ( $par =~ s{^([^|]+)\|}{} ) { + $linktext = $1; + } + + if( $par =~ m{^\w+://}s ) { + return make_URL_href( $par, $linktext ); + } + + # make sure sections start with a / + $par =~ s{^"}{/"}; + + my ( $page, $section, $ident ); + + # check for link patterns + if ( $par =~ m{^([^/]+?)/(?!")(.*?)$} ) { # name/ident + # we've got a name/ident (no quotes) + if ( length $2 ) { + ( $page, $ident ) = ( $1, $2 ); + } else { + ( $page, $section ) = ( $1, $2 ); + } + ### print STDERR "--> L<$par> to page $page, ident $ident\n"; + + } elsif ( $par =~ m{^(.*?)/"?(.*?)"?$} ) { # [name]/"section" + # even though this should be a "section", we go for ident first + ( $page, $ident ) = ( $1, $2 ); + ### print STDERR "--> L<$par> to page $page, section $section\n"; + + } elsif ( $par =~ /\s/ ) { # this must be a section with missing quotes + ( $page, $section ) = ( '', $par ); + ### print STDERR "--> L<$par> to void page, section $section\n"; + + } else { + ( $page, $section ) = ( $par, '' ); + ### print STDERR "--> L<$par> to page $par, void section\n"; + } + + # now, either $section or $ident is defined. the convoluted logic + # below tries to resolve L<> according to what the user specified. + # failing this, we try to find the next best thing... + my ( $url, $ltext, $fid ); + + RESOLVE: { + if ( defined $ident ) { + ## try to resolve $ident as an item + ( $url, $fid ) = coderef( $page, $ident ); + if ($url) { + if ( !defined($linktext) ) { + $linktext = $ident; + $linktext .= " in " if $ident && $page; + $linktext .= "the $page manpage" if $page; + } + ### print STDERR "got coderef url=$url\n"; + last RESOLVE; + } + ## no luck: go for a section (auto-quoting!) + $section = $ident; + } + ## now go for a section + my $htmlsection = htmlify($section); + $url = page_sect( $page, $htmlsection ); + if ($url) { + if ( !defined($linktext) ) { + $linktext = $section; + $linktext .= " in " if $section && $page; + $linktext .= "the $page manpage" if $page; + } + ### print STDERR "got page/section url=$url\n"; + last RESOLVE; + } + ## no luck: go for an ident + if ($section) { + $ident = $section; + } else { + $ident = $page; + $page = undef(); + } + ( $url, $fid ) = coderef( $page, $ident ); + if ($url) { + if ( !defined($linktext) ) { + $linktext = $ident; + $linktext .= " in " if $ident && $page; + $linktext .= "the $page manpage" if $page; + } + ### print STDERR "got section=>coderef url=$url\n"; + last RESOLVE; + } + + # warning; show some text. + $linktext = $opar unless defined $linktext; + warn +"$0: $Podfile: cannot resolve L<$opar> in paragraph $Paragraph.\n" + unless $Quiet; + } + + # now we have a URL or just plain code + $$rstr = $linktext . '>' . $$rstr; + if ( defined($url) ) { + $res = "<a href=\"$url\">" . process_text1( $lev, $rstr ) . '</a>'; + } else { + $res = '<em>' . process_text1( $lev, $rstr ) . '</em>'; + } + + } elsif ( $func eq 'S' ) { + + # S<text> - non-breaking spaces + $res = process_text1( $lev, $rstr ); + $res =~ s/ / /g; + + } elsif ( $func eq 'X' ) { + + # X<> - ignore + warn "$0: $Podfile: invalid X<> in paragraph $Paragraph.\n" + unless $$rstr =~ s/^[^>]*>// + or $Quiet; + } elsif ( $func eq 'Z' ) { + + # Z<> - empty + warn "$0: $Podfile: invalid Z<> in paragraph $Paragraph.\n" + unless $$rstr =~ s/^>// + or $Quiet; + + } else { + my $term = pattern $closing; + while ( $$rstr =~ s/\A(.*?)(([BCEFILSXZ])<(<+[^\S\n]+)?|$term)//s ) { + + # all others: either recurse into new function or + # terminate at closing angle bracket(s) + my $pt = $1; + $pt .= $2 if !$3 && $lev == 1; + $res .= $lev == 1 ? pure_text($pt) : inIS_text($pt); + return $res if !$3 && $lev > 1; + if ($3) { + $res .= process_text1( $lev, $rstr, $3, closing $4 ); + } + } + if ( $lev == 1 ) { + $res .= pure_text($$rstr); + } elsif ( !$Quiet ) { + my $snippet = substr( $$rstr, 0, 60 ); + warn +"$0: $Podfile: undelimited $func<> in paragraph $Paragraph: '$snippet'.\n" + + } + $res = process_text_rfc_links($res); + } + return $res; +} + +# +# go_ahead: extract text of an IS (can be nested) +# +sub go_ahead($$$) { + my ( $rstr, $func, $closing ) = @_; + my $res = ''; + my @closing = ($closing); + while ( $$rstr =~ + s/\A(.*?)(([BCEFILSXZ])<(<+\s+)?|@{[pattern $closing[0]]})//s ) + { + $res .= $1; + unless ($3) { + shift @closing; + return $res unless @closing; + } else { + unshift @closing, closing $4; + } + $res .= $2; + } + unless ($Quiet) { + my $snippet = substr( $$rstr, 0, 60 ); + warn +"$0: $Podfile: undelimited $func<> in paragraph $Paragraph (go_ahead): '$snippet'.\n"; + } + return $res; +} + +# +# emit_C - output result of C<text> +# $text is the depod-ed text +# +sub emit_C($;$$) { + my ( $text, $nocode, $args ) = @_; + $args = '' unless defined $args; + my $res; + my ( $url, $fid ) = coderef( undef(), $text ); + + # need HTML-safe text + my $linktext = html_escape("$text$args"); + + if ( $text !~ /^[\$@%]/ + && defined($url) + && ( !defined($EmittedItem) || $EmittedItem ne $fid ) ) + { + $res = "<a href=\"$url\"><code>$linktext</code></a>"; + } elsif ( 0 && $nocode ) { + $res = $linktext; + } else { + $res = "<code>$linktext</code>"; + } + return $res; +} + +# +# html_escape: make text safe for HTML +# +sub html_escape { + my $rest = $_[0]; + $rest =~ s/&/&/g; + $rest =~ s/</</g; + $rest =~ s/>/>/g; + $rest =~ s/"/"/g; + + # ' is only in XHTML, not HTML4. Be conservative + #$rest =~ s/'/'/g; + return $rest; +} + +# +# dosify - convert filenames to 8.3 +# +sub dosify { + my ($str) = @_; + return lc($str) if $^O eq 'VMS'; # VMS just needs casing + if ($Is83) { + $str = lc $str; + $str =~ s/(\.\w+)/substr ($1,0,4)/ge; + $str =~ s/(\w+)/substr ($1,0,8)/ge; + } + return $str; +} + +# +# page_sect - make a URL from the text of a L<> +# +sub page_sect($$) { + my ( $page, $section ) = @_; + my ( $linktext, $page83, $link ); # work strings + + # check if we know that this is a section in this page + if ( !defined $Pages{$page} && defined $Sections{$page} ) { + $section = $page; + $page = ""; + ### print STDERR "reset page='', section=$section\n"; + } + + $page83 = dosify($page); + $page = $page83 if ( defined $Pages{$page83} ); + if ( $page eq "" ) { + $link = "#" . anchorify($section); + } elsif ( $page =~ /::/ ) { + $page =~ s,::,/,g; + + # Search page cache for an entry keyed under the html page name, + # then look to see what directory that page might be in. NOTE: + # this will only find one page. A better solution might be to produce + # an intermediate page that is an index to all such pages. + my $page_name = $page; + $page_name =~ s,^.*/,,s; + if ( defined( $Pages{$page_name} ) + && $Pages{$page_name} =~ /([^:]*$page)\.(?:pod|pm):/ ) + { + $page = $1; + } else { + + # NOTE: This branch assumes that all A::B pages are located in + # $Htmlroot/A/B.html . This is often incorrect, since they are + # often in $Htmlroot/lib/A/B.html or such like. Perhaps we could + # analyze the contents of %Pages and figure out where any + # cousins of A::B are, then assume that. So, if A::B isn't found, + # but A::C is found in lib/A/C.pm, then A::B is assumed to be in + # lib/A/B.pm. This is also limited, but it's an improvement. + # Maybe a hints file so that the links point to the correct places + # nonetheless? + + } + $link = "$Htmlroot/$page.html"; + $link .= "#" . anchorify($section) if ($section); + } elsif ( !defined $Pages{$page} ) { + $link = ""; + } else { + $section = anchorify($section) if $section ne ""; + ### print STDERR "...section=$section\n"; + + # if there is a directory by the name of the page, then assume that an + # appropriate section will exist in the subdirectory + # if ($section ne "" && $Pages{$page} =~ /([^:]*[^(\.pod|\.pm)]):/) { + if ( $section ne "" && $Pages{$page} =~ /([^:]*(?<!\.pod)(?<!\.pm)):/ ) + { + $link = "$Htmlroot/$1/$section.html"; + ### print STDERR "...link=$link\n"; + + # since there is no directory by the name of the page, the section will + # have to exist within a .html of the same name. thus, make sure there + # is a .pod or .pm that might become that .html + } else { + $section = "#$section" if $section; + ### print STDERR "...section=$section\n"; + + # check if there is a .pod with the page name. + # for L<Foo>, Foo.(pod|pm) is preferred to A/Foo.(pod|pm) + if ( $Pages{$page} =~ /([^:]*)\.(?:pod|pm):/ ) { + $link = "$Htmlroot/$1.html$section"; + } else { + $link = ""; + } + } + } + + if ($link) { + + # Here, we take advantage of the knowledge that $Htmlfileurl ne '' + # implies $Htmlroot eq ''. This means that the link in question + # needs a prefix of $Htmldir if it begins with '/'. The test for + # the initial '/' is done to avoid '#'-only links, and to allow + # for other kinds of links, like file:, ftp:, etc. + my $url; + if ( $Htmlfileurl ne '' ) { + $link = "$Htmldir$link" if $link =~ m{^/}s; + $url = relativize_url( $link, $Htmlfileurl ); + + # print( " b: [$link,$Htmlfileurl,$url]\n" ); + } else { + $url = $link; + } + return $url; + + } else { + return undef(); + } +} + +# +# relativize_url - convert an absolute URL to one relative to a base URL. +# Assumes both end in a filename. +# +sub relativize_url { + my ( $dest, $source ) = @_; + + my ( $dest_volume, $dest_directory, $dest_file ) = + File::Spec::Unix->splitpath($dest); + $dest = File::Spec::Unix->catpath( $dest_volume, $dest_directory, '' ); + + my ( $source_volume, $source_directory, $source_file ) = + File::Spec::Unix->splitpath($source); + $source = + File::Spec::Unix->catpath( $source_volume, $source_directory, '' ); + + my $rel_path = ''; + if ( $dest ne '' ) { + $rel_path = File::Spec::Unix->abs2rel( $dest, $source ); + } + + if ( $rel_path ne '' + && substr( $rel_path, -1 ) ne '/' + && substr( $dest_file, 0, 1 ) ne '#' ) + { + $rel_path .= "/$dest_file"; + } else { + $rel_path .= "$dest_file"; + } + + return $rel_path; +} + +# +# coderef - make URL from the text of a C<> +# +sub coderef($$) { + my ( $page, $item ) = @_; + my ($url); + + my $fid = fragment_id($item); + + if ( defined($page) && $page ne "" ) { + + # we have been given a $page... + $page =~ s{::}{/}g; + + Carp::confess( +"Undefined fragment '$item' from fragment_id() in coderef() in $Podfile" + ) if !defined $fid; + + # Do we take it? Item could be a section! + my $base = $Items{$fid} || ""; + $base =~ s{[^/]*/}{}; + if ( $base ne "$page.html" ) { + ### print STDERR "coderef( $page, $item ): items{$fid} = $Items{$fid} = $base => discard page!\n"; + $page = undef(); + } + + } else { + + # no page - local items precede cached items + if ( defined($fid) ) { + if ( exists $Local_Items{$fid} ) { + $page = $Local_Items{$fid}; + } else { + $page = $Items{$fid}; + } + } + } + + # if there was a pod file that we found earlier with an appropriate + # =item directive, then create a link to that page. + if ( defined $page ) { + if ($page) { + if ( exists $Pages{$page} and $Pages{$page} =~ /([^:.]*)\.[^:]*:/ ) + { + $page = $1 . '.html'; + } + my $link = "$Htmlroot/$page#" . anchorify($fid); + + # Here, we take advantage of the knowledge that $Htmlfileurl + # ne '' implies $Htmlroot eq ''. + if ( $Htmlfileurl ne '' ) { + $link = "$Htmldir$link"; + $url = relativize_url( $link, $Htmlfileurl ); + } else { + $url = $link; + } + } else { + $url = "#" . anchorify($fid); + } + + confess "url has space: $url" if $url =~ /"[^"]*\s[^"]*"/; + } + return ( $url, $fid ); +} + +# +# Adapted from Nick Ing-Simmons' PodToHtml package. +sub relative_url { + my $source_file = shift; + my $destination_file = shift; + + my $source = URI::file->new_abs($source_file); + my $uo = URI::file->new( $destination_file, $source )->abs; + return $uo->rel->as_string; +} + +# +# finish_list - finish off any pending HTML lists. this should be called +# after the entire pod file has been read and converted. +# +sub finish_list { + while ( $Listlevel > 0 ) { + print HTML "</dl>\n"; + $Listlevel--; + } +} + +# +# htmlify - converts a pod section specification to a suitable section +# specification for HTML. Note that we keep spaces and special characters +# except ", ? (Netscape problem) and the hyphen (writer's problem...). +# +sub htmlify { + my ($heading) = @_; + $heading =~ s/(\s+)/ /g; + $heading =~ s/\s+\Z//; + $heading =~ s/\A\s+//; + + # The hyphen is a disgrace to the English language. + # $heading =~ s/[-"?]//g; + $heading =~ s/["?]//g; + $heading = lc($heading); + return $heading; +} + +# +# similar to htmlify, but turns non-alphanumerics into underscores +# +sub anchorify { + my ($anchor) = @_; + $anchor =~ s/\([^)]*\)//; + $anchor = htmlify($anchor); + $anchor =~ s/\W/_/g; + $anchor =~ tr/_/_/s; + return $anchor; +} + +# +# depod - convert text by eliminating all interior sequences +# Note: can be called with copy or modify semantics +# +my %E2c; +$E2c{lt} = '<'; +$E2c{gt} = '>'; +$E2c{sol} = '/'; +$E2c{verbar} = '|'; +$E2c{amp} = '&'; # in Tk's pods + +sub depod1($;$$); + +sub depod($) { + my $string; + if ( ref( $_[0] ) ) { + $string = ${ $_[0] }; + ${ $_[0] } = depod1( \$string ); + } else { + $string = $_[0]; + depod1( \$string ); + } +} + +sub depod1($;$$) { + my ( $rstr, $func, $closing ) = @_; + my $res = ''; + return $res unless defined $$rstr; + if ( !defined($func) ) { + + # skip to next begin of an interior sequence + while ( $$rstr =~ s/\A(.*?)([BCEFILSXZ])<(<+[^\S\n]+)?//s ) { + + # recurse into its text + $res .= $1 . depod1( $rstr, $2, closing $3); + } + $res .= $$rstr; + } elsif ( $func eq 'E' ) { + + # E<x> - convert to character + $$rstr =~ s/^([^>]*)>//; + $res .= $E2c{$1} || ""; + } elsif ( $func eq 'X' ) { + + # X<> - ignore + $$rstr =~ s/^[^>]*>//; + } elsif ( $func eq 'Z' ) { + + # Z<> - empty + $$rstr =~ s/^>//; + } else { + + # all others: either recurse into new function or + # terminate at closing angle bracket + my $term = pattern $closing; + while ( $$rstr =~ s/\A(.*?)(([BCEFILSXZ])<(<+[^\S\n]+)?|$term)//s ) { + $res .= $1; + last unless $3; + $res .= depod1( $rstr, $3, closing $4 ); + } + ## If we're here and $2 ne '>': undelimited interior sequence. + ## Ignored, as this is called without proper indication of where we are. + ## Rely on process_text to produce diagnostics. + } + return $res; +} + +{ + my %seen; # static fragment record hash + + sub fragment_id_readable { + my $text = shift; + my $generate = shift; # optional flag + + my $orig = $text; + + # leave the words for the fragment identifier, + # change everything else to underbars. + $text =~ + s/[^A-Za-z0-9_]+/_/g; # do not use \W to avoid locale dependency. + $text =~ s/_{2,}/_/g; + $text =~ s/\A_//; + $text =~ s/_\Z//; + + unless ($text) { + + # Nothing left after removing punctuation, so leave it as is + # E.g. if option is named: "=item -#" + + $text = $orig; + } + + if ($generate) { + if ( exists $seen{$text} ) { + + # This already exists, make it unique + $seen{$text}++; + $text = $text . $seen{$text}; + } else { + $seen{$text} = 1; # first time seen this fragment + } + } + + $text; + } +} + +my @HC; + +sub fragment_id_obfuscated { # This was the old "_2d_2d__" + my $text = shift; + my $generate = shift; # optional flag + + # text? Normalize by obfuscating the fragment id to make it unique + $text =~ s/\s+/_/sg; + + $text =~ s{(\W)}{ + defined( $HC[ord($1)] ) ? $HC[ord($1)] + : ( $HC[ord($1)] = sprintf( "%%%02X", ord($1) ) ) }gxe; + $text = substr( $text, 0, 50 ); + + $text; +} + +# +# fragment_id - construct a fragment identifier from: +# a) =item text +# b) contents of C<...> +# + +sub fragment_id { + my $text = shift; + my $generate = shift; # optional flag + + $text =~ s/\s+\Z//s; + if ($text) { + + # a method or function? + return $1 if $text =~ /(\w+)\s*\(/; + return $1 if $text =~ /->\s*(\w+)\s*\(?/; + + # a variable name? + return $1 if $text =~ /^([\$\@%*]\S+)/; + + # some pattern matching operator? + return $1 if $text =~ m|^(\w+/).*/\w*$|; + + # fancy stuff... like "do { }" + return $1 if $text =~ m|^(\w+)\s*{.*}$|; + + # honour the perlfunc manpage: func [PAR[,[ ]PAR]...] + # and some funnies with ... Module ... + return $1 if $text =~ m{^([a-z\d_]+)(\s+[A-Z,/& ][A-Z\d,/& ]*)?$}; + return $1 if $text =~ m{^([a-z\d]+)\s+Module(\s+[A-Z\d,/& ]+)?$}; + + return fragment_id_readable( $text, $generate ); + } else { + return; + } +} + +# +# make_URL_href - generate HTML href from URL +# Special treatment for CGI queries. +# +sub make_URL_href($;$) { + my ($url) = shift; + my $linktext = shift || $url; + if ( $url !~ s{^(http:[-\w/#~:.+=&%@!]+)(\?.*)$}{<a href="$1$2">$1</a>}i ) { + $url = "<a href=\"$url\">$linktext</a>"; + } + return $url; +} + +1; diff --git a/plugins/perl/lib/Xchat.pm b/plugins/perl/lib/Xchat.pm new file mode 100644 index 00000000..74914882 --- /dev/null +++ b/plugins/perl/lib/Xchat.pm @@ -0,0 +1,506 @@ +BEGIN { + $INC{'Xchat.pm'} = 'DUMMY'; +} + +$SIG{__WARN__} = sub { + my $message = shift @_; + my ($package) = caller; + + # redirect Gtk/Glib errors and warnings back to STDERR + my $message_levels = qr/ERROR|CRITICAL|WARNING|MESSAGE|INFO|DEBUG/i; + if( $message =~ /^(?:Gtk|GLib|Gdk)(?:-\w+)?-$message_levels/i ) { + print STDERR $message; + } else { + + if( defined &Xchat::Internal::print ) { + Xchat::print( $message ); + } else { + warn $message; + } + } +}; + +use File::Spec (); +use File::Basename (); +use File::Glob (); +use List::Util (); +use Symbol(); +use Time::HiRes (); +use Carp (); + +package Xchat; +use base qw(Exporter); +use strict; +use warnings; + +sub PRI_HIGHEST (); +sub PRI_HIGH (); +sub PRI_NORM (); +sub PRI_LOW (); +sub PRI_LOWEST (); + +sub EAT_NONE (); +sub EAT_XCHAT (); +sub EAT_PLUIN (); +sub EAT_ALL (); + +sub KEEP (); +sub REMOVE (); +sub FD_READ (); +sub FD_WRITE (); +sub FD_EXCEPTION (); +sub FD_NOTSOCKET (); + +sub get_context; +sub Xchat::Internal::context_info; +sub Xchat::Internal::print; + +our %EXPORT_TAGS = ( + constants => [ + qw(PRI_HIGHEST PRI_HIGH PRI_NORM PRI_LOW PRI_LOWEST), # priorities + qw(EAT_NONE EAT_XCHAT EAT_PLUGIN EAT_ALL), # callback return values + qw(FD_READ FD_WRITE FD_EXCEPTION FD_NOTSOCKET), # fd flags + qw(KEEP REMOVE), # timers + ], + hooks => [ + qw(hook_server hook_command hook_print hook_timer hook_fd unhook), + ], + util => [ + qw(register nickcmp strip_code send_modes), # misc + qw(print prnt printf prntf command commandf emit_print), # output + qw(find_context get_context set_context), # context + qw(get_info get_prefs get_list context_info user_info), # input + ], +); + +$EXPORT_TAGS{all} = [ map { @{$_} } @EXPORT_TAGS{qw(constants hooks util)}]; +our @EXPORT = @{$EXPORT_TAGS{constants}}; +our @EXPORT_OK = @{$EXPORT_TAGS{all}}; + +sub register { + my $package = Xchat::Embed::find_pkg(); + my $pkg_info = Xchat::Embed::pkg_info( $package ); + my $filename = $pkg_info->{filename}; + my ($name, $version, $description, $callback) = @_; + + if( defined $pkg_info->{gui_entry} ) { + Xchat::print( "Xchat::register called more than once in " + . $pkg_info->{filename} ); + return (); + } + + $description = "" unless defined $description; + $pkg_info->{shutdown} = $callback; + unless( $name && $name =~ /[[:print:]\w]/ ) { + $name = "Not supplied"; + } + unless( $version && $version =~ /\d+(?:\.\d+)?/ ) { + $version = "NaN"; + } + $pkg_info->{gui_entry} = + Xchat::Internal::register( $name, $version, $description, $filename ); + # keep with old behavior + return (); +} + +sub _process_hook_options { + my ($options, $keys, $store) = @_; + + unless( @$keys == @$store ) { + die 'Number of keys must match the size of the store'; + } + + my @results; + + if( ref( $options ) eq 'HASH' ) { + for my $index ( 0 .. @$keys - 1 ) { + my $key = $keys->[$index]; + if( exists( $options->{ $key } ) && defined( $options->{ $key } ) ) { + ${$store->[$index]} = $options->{ $key }; + } + } + } + +} + +sub hook_server { + return undef unless @_ >= 2; + my $message = shift; + my $callback = shift; + my $options = shift; + my $package = Xchat::Embed::find_pkg(); + + $callback = Xchat::Embed::fix_callback( $package, $callback ); + + my ($priority, $data) = ( Xchat::PRI_NORM, undef ); + _process_hook_options( + $options, + [qw(priority data)], + [\($priority, $data)], + ); + + my $pkg_info = Xchat::Embed::pkg_info( $package ); + my $hook = Xchat::Internal::hook_server( + $message, $priority, $callback, $data + ); + push @{$pkg_info->{hooks}}, $hook if defined $hook; + return $hook; +} + +sub hook_command { + return undef unless @_ >= 2; + my $command = shift; + my $callback = shift; + my $options = shift; + my $package = Xchat::Embed::find_pkg(); + + $callback = Xchat::Embed::fix_callback( $package, $callback ); + + my ($priority, $help_text, $data) = ( Xchat::PRI_NORM, undef, undef ); + _process_hook_options( + $options, + [qw(priority help_text data)], + [\($priority, $help_text, $data)], + ); + + my $pkg_info = Xchat::Embed::pkg_info( $package ); + my $hook = Xchat::Internal::hook_command( + $command, $priority, $callback, $help_text, $data + ); + push @{$pkg_info->{hooks}}, $hook if defined $hook; + return $hook; +} + +sub hook_print { + return undef unless @_ >= 2; + my $event = shift; + my $callback = shift; + my $options = shift; + my $package = Xchat::Embed::find_pkg(); + + $callback = Xchat::Embed::fix_callback( $package, $callback ); + + my ($priority, $run_after, $filter, $data) = ( Xchat::PRI_NORM, 0, 0, undef ); + _process_hook_options( + $options, + [qw(priority run_after_event filter data)], + [\($priority, $run_after, $filter, $data)], + ); + + if( $run_after and $filter ) { + Carp::carp( "Xchat::hook_print's run_after_event and filter options are mutually exclusive, you can only use of them at a time per hook" ); + return; + } + + if( $run_after ) { + my $cb = $callback; + $callback = sub { + my @args = @_; + hook_timer( 0, sub { + $cb->( @args ); + + if( ref $run_after eq 'CODE' ) { + $run_after->( @args ); + } + return REMOVE; + }); + return EAT_NONE; + }; + } + + if( $filter ) { + my $cb = $callback; + $callback = sub { + my @args = @{$_[0]}; + my $last_arg = @args - 1; + + my @new = $cb->( \@args, $_[1], $event ); + + # a filter can either return the new results or it can modify + # @_ in place. + if( @new ) { + emit_print( $event, @new[ 0 .. $last_arg ] ); + return EAT_ALL; + } elsif( + join( "\0", @{$_[0]} ) ne join( "\0", @args[ 0 .. $last_arg ] ) + ) { + emit_print( $event, @args[ 0 .. $last_arg ] ); + return EAT_ALL; + } + + return EAT_NONE; + }; + + } + + my $pkg_info = Xchat::Embed::pkg_info( $package ); + my $hook = Xchat::Internal::hook_print( + $event, $priority, $callback, $data + ); + push @{$pkg_info->{hooks}}, $hook if defined $hook; + return $hook; +} + +sub hook_timer { + return undef unless @_ >= 2; + my ($timeout, $callback, $data) = @_; + my $package = Xchat::Embed::find_pkg(); + + $callback = Xchat::Embed::fix_callback( $package, $callback ); + + if( + ref( $data ) eq 'HASH' && exists( $data->{data} ) + && defined( $data->{data} ) + ) { + $data = $data->{data}; + } + + my $pkg_info = Xchat::Embed::pkg_info( $package ); + my $hook = Xchat::Internal::hook_timer( $timeout, $callback, $data, $package ); + push @{$pkg_info->{hooks}}, $hook if defined $hook; + return $hook; +} + +sub hook_fd { + return undef unless @_ >= 2; + my ($fd, $callback, $options) = @_; + return undef unless defined $fd && defined $callback; + + my $fileno = fileno $fd; + return undef unless defined $fileno; # no underlying fd for this handle + + my ($package) = Xchat::Embed::find_pkg(); + $callback = Xchat::Embed::fix_callback( $package, $callback ); + + my ($flags, $data) = (Xchat::FD_READ, undef); + _process_hook_options( + $options, + [qw(flags data)], + [\($flags, $data)], + ); + + my $cb = sub { + my $userdata = shift; + return $userdata->{CB}->( + $userdata->{FD}, $userdata->{FLAGS}, $userdata->{DATA}, + ); + }; + + my $pkg_info = Xchat::Embed::pkg_info( $package ); + my $hook = Xchat::Internal::hook_fd( + $fileno, $cb, $flags, { + DATA => $data, FD => $fd, CB => $callback, FLAGS => $flags, + } + ); + push @{$pkg_info->{hooks}}, $hook if defined $hook; + return $hook; +} + +sub unhook { + my $hook = shift @_; + my $package = shift @_; + ($package) = caller unless $package; + my $pkg_info = Xchat::Embed::pkg_info( $package ); + + if( defined( $hook ) + && $hook =~ /^\d+$/ + && grep { $_ == $hook } @{$pkg_info->{hooks}} ) { + $pkg_info->{hooks} = [grep { $_ != $hook } @{$pkg_info->{hooks}}]; + return Xchat::Internal::unhook( $hook ); + } + return (); +} + +sub _do_for_each { + my ($cb, $channels, $servers) = @_; + + # not specifying any channels or servers is not the same as specifying + # undef for both + # - not specifying either results in calling the callback inthe current ctx + # - specifying undef for for both results in calling the callback in the + # front/currently selected tab + if( @_ == 3 && !($channels || $servers) ) { + $channels = [ undef ]; + $servers = [ undef ]; + } elsif( !($channels || $servers) ) { + $cb->(); + return 1; + } + + $channels = [ $channels ] unless ref( $channels ) eq 'ARRAY'; + + if( $servers ) { + $servers = [ $servers ] unless ref( $servers ) eq 'ARRAY'; + } else { + $servers = [ undef ]; + } + + my $num_done = 0; + my $old_ctx = Xchat::get_context(); + for my $server ( @$servers ) { + for my $channel ( @$channels ) { + if( Xchat::set_context( $channel, $server ) ) { + $cb->(); + $num_done++ + } + } + } + Xchat::set_context( $old_ctx ); + return $num_done; +} + +sub print { + my $text = shift @_; + return "" unless defined $text; + if( ref( $text ) eq 'ARRAY' ) { + if( $, ) { + $text = join $, , @$text; + } else { + $text = join "", @$text; + } + } + + return _do_for_each( + sub { Xchat::Internal::print( $text ); }, + @_ + ); +} + +sub printf { + my $format = shift; + Xchat::print( sprintf( $format, @_ ) ); +} + +# make Xchat::prnt() and Xchat::prntf() as aliases for Xchat::print() and +# Xchat::printf(), mainly useful when these functions are exported +sub prnt { + goto &Xchat::print; +} + +sub prntf { + goto &Xchat::printf; +} + +sub command { + my $command = shift; + return "" unless defined $command; + my @commands; + + if( ref( $command ) eq 'ARRAY' ) { + @commands = @$command; + } else { + @commands = ($command); + } + + return _do_for_each( + sub { Xchat::Internal::command( $_ ) foreach @commands }, + @_ + ); +} + +sub commandf { + my $format = shift; + Xchat::command( sprintf( $format, @_ ) ); +} + +sub set_context { + my $context; + if( @_ == 2 ) { + my ($channel, $server) = @_; + $context = Xchat::find_context( $channel, $server ); + } elsif( @_ == 1 ) { + if( defined $_[0] && $_[0] =~ /^\d+$/ ) { + $context = $_[0]; + } else { + $context = Xchat::find_context( $_[0] ); + } + } elsif( @_ == 0 ) { + $context = Xchat::find_context(); + } + return $context ? Xchat::Internal::set_context( $context ) : 0; +} + +sub get_info { + my $id = shift; + my $info; + + if( defined( $id ) ) { + if( grep { $id eq $_ } qw(state_cursor id) ) { + $info = Xchat::get_prefs( $id ); + } else { + $info = Xchat::Internal::get_info( $id ); + } + } + return $info; +} + +sub user_info { + my $nick = Xchat::strip_code(shift @_ || Xchat::get_info( "nick" )); + my $user; + for (Xchat::get_list( "users" ) ) { + if ( Xchat::nickcmp( $_->{nick}, $nick ) == 0 ) { + $user = $_; + last; + } + } + return $user; +} + +sub context_info { + my $ctx = shift @_ || Xchat::get_context; + my $old_ctx = Xchat::get_context; + my @fields = ( + qw(away channel charset host id inputbox libdirfs modes network), + qw(nick nickserv server topic version win_ptr win_status), + qw(xchatdir xchatdirfs state_cursor), + ); + + if( Xchat::set_context( $ctx ) ) { + my %info; + for my $field ( @fields ) { + $info{$field} = Xchat::get_info( $field ); + } + + my $ctx_info = Xchat::Internal::context_info; + @info{keys %$ctx_info} = values %$ctx_info; + + Xchat::set_context( $old_ctx ); + return %info if wantarray; + return \%info; + } else { + return undef; + } +} + +sub get_list { + unless( grep { $_[0] eq $_ } qw(channels dcc ignore notify users networks) ) { + Carp::carp( "'$_[0]' does not appear to be a valid list name" ); + } + if( $_[0] eq 'networks' ) { + return Xchat::List::Network->get(); + } else { + return Xchat::Internal::get_list( $_[0] ); + } +} + +sub strip_code { + my $pattern = qr< + \cB| #Bold + \cC\d{0,2}(?:,\d{1,2})?| #Color + \e\[(?:\d{1,2}(?:;\d{1,2})*)?m| # ANSI color code + \cG| #Beep + \cO| #Reset + \cV| #Reverse + \c_ #Underline + >x; + + if( defined wantarray ) { + my $msg = shift; + $msg =~ s/$pattern//g; + return $msg; + } else { + $_[0] =~ s/$pattern//g if defined $_[0]; + } +} + +1 diff --git a/plugins/perl/lib/Xchat.pod b/plugins/perl/lib/Xchat.pod new file mode 100644 index 00000000..a55a9bce --- /dev/null +++ b/plugins/perl/lib/Xchat.pod @@ -0,0 +1,1326 @@ +=head1 X-Chat 2 Perl Interface + +=head2 Introduction + +This is the new Perl interface for X-Chat 2. However, due to changes in +xchat's plugin code you will need xchat 2.0.8 or above to load this. Scripts +written using the old interface will continue to work. If there are any +problems, questions, comments or suggestions please email them to the address +on the bottom of this page. + +=head2 Constants + +=head3 Priorities + +=over 3 + +=item * +C<Xchat::PRI_HIGHEST> + +=item * +C<Xchat::PRI_HIGH> + +=item * +C<Xchat::PRI_NORM> + +=item * +C<Xchat::PRI_LOW> + +=item * +C<Xchat::PRI_LOWEST> + +=back + +=head3 Return values + +=over 3 + +=item * +C<Xchat::EAT_NONE> - pass the event along + +=item * +C<Xchat::EAT_XCHAT> - don't let xchat see this event + +=item * +C<Xchat::EAT_PLUGIN> - don't let other scripts and plugins see this event but xchat will still see it + +=item * +C<Xchat::EAT_ALL> - don't let anything else see this event + +=back + +=head4 Timer and fd hooks + +=over 3 + +=item * +C<Xchat::KEEP> - keep the timer going or hook watching the handle + +=item * +C<Xchat::REMOVE> - remove the timer or hook watching the handle + +=back + +=head3 hook_fd flags + +=over 3 + +=item * +C<Xchat::FD_READ> - invoke the callback when the handle is ready for reading + +=item * +C<Xchat::FD_WRITE> - invoke the callback when the handle is ready for writing + +=item * +C<Xchat::FD_EXCEPTION> - invoke the callback if an exception occurs + +=item * +C<Xchat::FD_NOTSOCKET> - indicate that the handle being hooked is not a socket + +=back + +=head2 Functions + +=head3 C<Xchat::register( $name, $version, [$description,[$callback]] )> + +=over 3 + +=item * +C<$name> - The name of this script + +=item * +C<$version> - This script's version + +=item * +C<$description> - A description for this script + +=item * +C<$callback> - This is a function that will be called when the is script + unloaded. This can be either a reference to a + function or an anonymous sub reference. + +=back + +This is the first thing to call in every script. + +=head3 C<Xchat::hook_server( $message, $callback, [\%options] )> + +=head3 C<Xchat::hook_command( $command, $callback, [\%options] )> + +=head3 C<Xchat::hook_print( $event,$callback, [\%options] )> + +=head3 C<Xchat::hook_timer( $timeout,$callback, [\%options | $data] )> + +=head3 C<Xchat::hook_fd( $handle, $callback, [ \%options ] )> + +These functions can be to intercept various events. +hook_server can be used to intercept any incoming message from the IRC server. +hook_command can be used to intercept any command, if the command doesn't currently exist then a new one is created. +hook_print can be used to intercept any of the events listed in Setttings-E<gt>Advanced-E<gt>Text Events +hook_timer can be used to create a new timer + + +=over 3 + +=item * +C<$message> - server message to hook such as PRIVMSG + +=item * +C<$command> - command to intercept, without the leading / + +=item * +C<$event> - one of the events listed in Settings-E<gt>Advanced-E<gt>Text Events + +=item * +C<$timeout> - timeout in milliseconds + +=item * +C<$handle> - the I/O handle you want to monitor with hook_fd. This must be something that has a fileno. See perldoc -f fileno or L<fileno|http://perldoc.perl.org/functions/fileno.html> + +=item * +C<$callback> - callback function, this is called whenever + the hooked event is trigged, the following are + the conditions that will trigger the different hooks. + This can be either a reference to a + function or an anonymous sub reference. + +=item * +\%options - a hash reference containing addional options for the hooks + +=back + +Valid keys for \%options: + +=begin html + +<table border="1"> + + <tr> + <td>data</td> <td>Additional data that is to be associated with the<br /> + hook. For timer hooks this value can be provided either as<br /> + <code>Xchat::hook_timer( $timeout, $cb,{data=>$data})</code><br /> + or <code>Xchat::hook_timer( $timeout, $cb, $data )</code>.<br /> + However, this means that hook_timer cannot be provided<br /> + with a hash reference containing data as a key.<br /> + + example:<br /> + my $options = { data => [@arrayOfStuff] };<br /> + Xchat::hook_timer( $timeout, $cb, $options );<br /> + <br /> + In this example, the timer's data will be<br /> + [@arrayOfStuff] and not { data => [@arrayOfStuff] }<br /> + <br /> + This key is valid for all of the hook functions.<br /> + <br /> + Default is undef.<br /> + </td> + </tr> + + <tr> + <td>priority</td> <td>Sets the priority for the hook.<br /> + It can be set to one of the + <code>Xchat::PRI_*</code> constants.<br /> + <br /> + This key only applies to server, command + and print hooks.<br /> + <br /> + Default is <code>Xchat::PRI_NORM</code>. + </td> + + </tr> + + <tr> + <td>help_text</td> <td>Text displayed for /help $command.<br /> + <br /> + This key only applies to command hooks.<br /> + <br /> + Default is "". + </td> + </tr> + + <tr> + <td>flags</td> <td>Specify the flags for a fd hook.<br /> + <br /> + See <a href="#hook_fd_flags">hook fd flags</a> section for valid values.<br /> + <br /> + On Windows if the handle is a pipe you specify<br /> + Xchat::FD_NOTSOCKET in addition to any other flags you might be using.<br /> + <br /> + This key only applies to fd hooks.<br /> + Default is Xchat::FD_READ + </td> + </tr> + +</table> + +=end html + +=head4 When callbacks are invoked + +Each of the hooks will be triggered at different times depending on the type +of hook. + +=begin html + +<table border="1"> + + <tr style="background-color: #dddddd"> + <td>Hook Type</td> <td>When the callback will be invoked</td> + </tr> + + <tr> + <td>server hooks</td> <td>a <code>$message</code> message is + received from the server + </td> + </tr> + + <tr> + <td>command hooks</td> <td>the <code>$command</code> command is + executed, either by the user or from a script + </td> + </tr> + + <tr> + <td>print hooks</td> <td>X-Chat is about to print the message for the + <code>$event</code> event + </td> + </tr> + + <tr> + <td>timer hooks</td> <td>called every <code>$timeout</code> milliseconds + (1000 millisecond is 1 second)<br /> + the callback will be executed in the same context where + the hook_timer was called, if the context no longer exists + then it will execute in a random context + </td> + </tr> + + <tr> + <td>fd hooks</td> <td>depends on the flags that were passed to hook_fd<br /> + See <a href="#hook_fd_flags">hook_fd flags</a> section. + </td> + </tr> +</table> + +=end html + + +The value return from these hook functions can be passed to C<Xchat::unhook> +to remove the hook. + +=head4 Callback Arguments + +All callback functions will receive their arguments in C<@_> like every +other Perl subroutine. + +=begin html + +<p> +Server and command callbacks<br /> +<br /> +<code>$_[0]</code> - array reference containing the IRC message or command and +arguments broken into words<br /> +example:<br /> +/command arg1 arg2 arg3<br /> +<code>$_[0][0]</code> - command<br /> +<code>$_[0][1]</code> - arg1<br /> +<code>$_[0][2]</code> - arg2<br /> +<code>$_[0][3]</code> - arg3<br /> +<br /> +<code>$_[1]</code> - array reference containing the Nth word to the last word<br /> +example:<br /> +/command arg1 arg2 arg3<br /> +<code>$_[1][0]</code> - command arg1 arg2 arg3<br /> +<code>$_[1][1]</code> - arg1 arg2 arg3<br /> +<code>$_[1][2]</code> - arg2 arg3<br /> +<code>$_[1][3]</code> - arg3<br /> +<br /> +<code>$_[2]</code> - the data that was passed to the hook function<br /> +<br /> +Print callbacks<br /> +<br /> +<code>$_[0]</code> - array reference containing the values for the + text event see Settings->Advanced->Text Events<br /> +<code>$_[1]</code> - the data that was passed to the hook function<br /> +<br /> +Timer callbacks<br /> +<br /> +<code>$_[0]</code> - the data that was passed to the hook function<br /> +<br /> + +fd callbacks<br /> +<br /> +<code>$_[0]</code> - the handle that was passed to hook_fd<br /> +<code>$_[1]</code> - flags indicating why the callback was called<br /> +<code>$_[2]</code> - the data that was passed to the hook function<br /> +</p> + +=end html + +=head4 Callback return values + +All server, command and print callbacks should return one of +the C<Xchat::EAT_*> constants. +Timer callbacks can return Xchat::REMOVE to remove +the timer or Xchat::KEEP to keep it going + +=head4 Miscellaneous Hook Related Information + +For server hooks, if C<$message> is "RAW LINE" then C<$cb> will be called for +every IRC message than X-Chat receives. + +For command hooks if C<$command> is "" then C<$cb> will be called for +messages entered by the user that is not a command. + +For print hooks besides those events listed in +Settings-E<gt>Advanced-E<gt>Text Events, these additional events can be used. + +=begin html + +<table border="1"> + + <tr style="background-color: #dddddd"> + <td>Event</td> <td>Description</td> + </tr> + + <tr> + <td>"Open Context"</td> <td>a new context is created</td> + </tr> + + <tr> + <td>"Close Context"</td> <td>a context has been close</td> + </tr> + + <tr> + <td>"Focus Tab"</td> <td>when a tab is brought to the front</td> + </tr> + + <tr> + <td>"Focus Window"</td> <td>when a top level window is focused or the + main tab window is focused by the window manager + </td> + </tr> + + <tr> + <td>"DCC Chat Text"</td> <td>when text from a DCC Chat arrives. + <code>$_[0]</code> will have these values<br /> + <br /> + <code>$_[0][0]</code> - Address<br /> + <code>$_[0][1]</code> - Port<br /> + <code>$_[0][2]</code> - Nick<br /> + <code>$_[0][3]</code> - Message<br /> + </td> + </tr> + + <tr> + <td>"Key Press"</td> <td>used for intercepting key presses<br /> + $_[0][0] - key value<br /> + $_[0][1] - state bitfield, 1 - shift, 4 - control, 8 - alt<br /> + $_[0][2] - string version of the key which might be empty for unprintable keys<br /> + $_[0][3] - length of the string in $_[0][2]<br /> + </td> + </tr> +</table> + +=end html + +=head3 C<Xchat::unhook( $hook )> + +=over 3 + +=item * +C<$hook> - the hook that was previously returned by one of the C<Xchat::hook_*> functions + +=back + + +This function is used to removed a hook previously added with one of +the C<Xchat::hook_*> functions + +It returns the data that was passed to the C<Xchat::hook_*> function when +the hook was added + + +=head3 C<Xchat::print( $text | \@lines, [$channel,[$server]] )> + +=over 3 + +=item * +C<$text> - the text to print + +=item * +C<\@lines> - array reference containing lines of text to be printed + all the elements will be joined together before printing + +=item * +C<$channel> - channel or tab with the given name where C<$text> + will be printed + +=item * +C<$server> - specifies that the text will be printed in a channel or tab + that is associated with C<$server> + +=back + +The first argument can either be a string or an array reference of strings. +Either or both of C<$channel> and C<$server> can be undef. + +If called as C<Xchat::print( $text )>, it will always return true. +If called with either the channel or the channel and the server +specified then it will return true if a context is found and +false otherwise. The text will not be printed if the context +is not found. The meaning of setting C<$channel> or C<$server> to +undef is the same as +L<find_context|xchat_find_context>. + + +=head3 C<Xchat::printf( $format, LIST )> + +=over 3 + +=item * +C<$format> - a format string, see "perldoc -f L<sprintf|http://perldoc.perl.org/functions/sprintf.html>" for further detail + +=item * +LIST - list of values for the format fields + +=back + +=head3 C<Xchat::command( $command | \@commands, [$channel,[$server]] )> + +=over 3 + +=item * +C<$command> - the command to execute, without the leading / + +=item * +C<\@commands> - array reference containing a list of commands to execute + +=item * +C<$channel> - channel or tab with the given name where C<$command> will be executed + +=item * +C<$server> - specifies that the command will be executed in a channel or tab that is associated with C<$server> + +=back + +The first argument can either be a string or an array reference of strings. +Either or both of C<$channel> and C<$server> can be undef. + +If called as C<Xchat::command( $command )>, it will always return true. +If called with either the channel or the channel and the server +specified then it will return true if a context is found and false +otherwise. The command will not be executed if the context is not found. +The meaning of setting C<$channel> or C<$server> to undef is the same +as find_context. + + +=head3 C<Xchat::commandf( $format, LIST )> + +=over 3 + +=item * +C<$format> - a format string, see "perldoc -f L<sprintf|http://perldoc.perl.org/functions/sprintf.html>" for further detail + +=item * +LIST - list of values for the format fields + +=back + +=head3 C<Xchat::find_context( [$channel, [$server]] )> + +=over 3 + +=item * +C<$channel> - name of a channel + +=item * +C<$server> - name of a server + +=back + +Either or both of C<$channel> and $server can be undef. Calling +C<Xchat::find_context()> is the same as calling +C<Xchat::find_context( undef, undef)> and +C<Xchat::find_context( $channel )> is +the same as C<Xchat::find_context( $channel, undef )>. + +If C<$server> is undef, find any channel named $channel. +If C<$channel> is undef, find the front most window +or tab named C<$server>.If both $channel and +C<$server> are undef, find the currently focused tab or window. + +Return the context found for one of the above situations or undef if such +a context cannot be found. + + +=head3 C<Xchat::get_context()> + +=over 3 + +=back + +Returns the current context. + +=head3 C<Xchat::set_context( $context | $channel,[$server] )> + +=over 3 + +=item * +C<$context> - context value as returned from L<get_context|xchat_get_context>,L<find_context|xchat_find_context> or one + of the fields in the list of hashrefs returned by list_get + +=item * +C<$channel> - name of a channel you want to switch context to + +=item * +C<$server> - name of a server you want to switch context to + +=back + +See L<find_context|xchat_find_context> for more details on C<$channel> and C<$server>. + +Returns true on success, false on failure + +=head3 C<Xchat::get_info( $id )> + +=over 3 + +=item * +C<$id> - one of the following case sensitive values + +=back + +=begin html + +<table border="1"> + + <tr style="background-color: #dddddd"> + <td>ID</td> <td>Return value</td> + </tr> + + <tr> + <td>away</td> <td>away reason or undef if you are not away</td> + </tr> + + <tr> + <td>channel</td> <td>current channel name</td> + </tr> + + <tr> + <td>charset</td> <td>character-set used in the current context</td> + </tr> + + <tr> + <td>event_text <Event Name></td> <td>text event format string for <Event name><br /> + Example: + <div class="example"> + my $channel_msg_format = Xchat::get_info( "event_text Channel Message" ); + </div> + </td> +</tr> +<tr> + <td>host</td> <td>real hostname of the current server</td> +</tr> + +<tr> + <td>id</td> <td>connection id</td> +</tr> + +<tr> + <td>inputbox</td> <td>contents of the inputbox</td> +</tr> + +<tr> + <td>libdirfs</td> + <td>the system wide directory where xchat will look for plugins. + this string is in the same encoding as the local file system</td> +</tr> + +<tr> + <td>modes</td> <td>the current channels modes or undef if not known</td> +</tr> + +<tr> + <td>network</td> <td>current network name or undef</td> +</tr> + +<tr> + <td>nick</td> <td>current nick</td> +</tr> + +<tr> + <td>nickserv</td> <td>nickserv password for this network or undef</td> +</tr> + +<tr> + <td>server</td> <td>current server name <br /> + (what the server claims to be) undef if not connected + </td> +</tr> + +<tr> + <td>state_cursor</td> + <td>current inputbox cursor position in characters</td> +</tr> + +<tr> + <td>topic</td> <td>current channel topic</td> +</tr> + +<tr> + <td>version</td> <td>xchat version number</td> +</tr> + +<tr> + <td>win_status</td> + <td>status of the xchat window, possible values are "active", "hidden" + and "normal"</td> +</tr> + +<tr> + <td>win_ptr</td> <td>native window pointer, GtkWindow * on Unix, HWND on Win32.<br /> + On Unix if you have the Glib module installed you can use my $window = Glib::Object->new_from_pointer( Xchat::get_info( "win_ptr" ) ); to get a Gtk2::Window object.<br /> + Additionally when you have detached tabs, each of the windows will return a different win_ptr for the different Gtk2::Window objects.<br /> + See <a href="http://xchat.cvs.sourceforge.net/viewvc/xchat/xchat2/plugins/perl/char_count.pl?view=markup">char_count.pl</a> for a longer example of a script that uses this to show how many characters you currently have in your input box. + </td> +</tr> +<tr> + <td>xchatdir</td> <td>xchat config directory encoded in UTF-8<br /> + examples:<br /> + /home/user/.xchat2<br /> + C:\Documents and Settings\user\Application Data\X-Chat 2 + </td> +</tr> + +<tr> + <td>xchatdirfs</td> <td>same as xchatdir except encoded in the locale file system encoding</td> +</tr> +</table> + +<p>This function is used to retrieve certain information about the current +context.</p> + +=end html + +=head3 C<Xchat::get_prefs( $name )> + +=over 3 + +=item * +C<$name> - name of a X-Chat setting (available through the /set command) + +=back + +This function provides a way to retrieve X-Chat's setting information. + +Returns C<undef> if there is no setting called called C<$name>. + + +=head3 C<Xchat::emit_print( $event, LIST )> + +=over 3 + +=item * +C<$event> - name from the Event column in Settings-E<gt>Advanced-E<gt>Text Events + +=item * +LIST - this depends on the Description column on the bottom of Settings-E<gt>Advanced-E<gt>Text Events + +=back + +This functions is used to generate one of the events listed under +Settings-E<gt>Advanced-E<gt>Text Events + +Note: when using this function you MUST return Xchat::EAT_ALL otherwise you will end up with duplicate events. +One is the original and the second is the one you emit. + +Returns true on success, false on failure + +=head3 C<Xchat::send_modes( $target | \@targets, $sign, $mode, [ $modes_per_line ] )> + +=over 3 + +=item * +C<$target> - a single nick to set the mode on + +=item * +C<\@targets> - an array reference of the nicks to set the mode on + +=item * +C<$sign> - the mode sign, either '+' or '-' + +=item * +C<$mode> - the mode character such as 'o' and 'v', this can only be one character long + +=item * +C<$modes_per_line> - an optional argument maximum number of modes to send per at once, pass 0 use the current server's maximum (default) + +=back + +Send multiple mode changes for the current channel. It may send multiple MODE lines if the request doesn't fit on one. + +Example: + +=begin html +<<div class="example"> +<code> +use strict; +use warning; +use Xchat qw(:all); + +hook_command( "MODES", sub { + my (undef, $who, $sign, $mode) = @{$_[0]}; + + my @targets = split /,/, $who; + if( @targets > 1 ) { + send_modes( \@targets, $sign, $mode, 1 ); + } else { + send_modes( $who, $sign, $mode ); + } + + return EAT_XCHAT; +}); + +</code> +</div> + +=end html + +=head3 C<Xchat::nickcmp( $nick1, $nick2 )> + +=over 3 + +=item * +C<$nick1, $nick2> - the two nicks or channel names that are to be compared + +=back + +The comparsion is based on the current server. Either a RFC1459 compliant +string compare or plain ascii will be using depending on the server. The +comparison is case insensitive. + +Returns a number less than, equal to or greater than zero if +C<$nick1> is +found respectively, to be less than, to match, or be greater than +C<$nick2>. + + +=head3 C<Xchat::get_list( $name )> + +=over 3 + +=item * +C<$name> - name of the list, one of the following: +"channels", "dcc", "ignore", "notify", "users" + +=back + +This function will return a list of hash references. The hash references +will have different keys depend on the list. An empty list is returned +if there is no such list. + +=begin html + +<p>"channels" - list of channels, querys and their server</p> + +<table border="1"> + + <tr style="background-color: #dddddd"> + <td>Key</td> <td>Description</td> + </tr> + + <tr> + <td>channel</td> <td>tab name</td> + </tr> + + <tr> + <td>chantypes</td> + <td>channel types supported by the server, typically "#&"</td> + </tr> + + <tr> + <td>context</td> <td>can be used with set_context</td> + </tr> + + <tr> + <td>flags</td> <td>Server Bits:<br /> + 0 - Connected<br /> + 1 - Connecting<br /> + 2 - Away<br /> + 3 - EndOfMotd(Login complete)<br /> + 4 - Has WHOX<br /> + 5 - Has IDMSG (FreeNode)<br /> + <br /> + <p>The following correspond to the /chanopt command</p> + 6 - Hide Join/Part Message (text_hidejoinpart)<br /> + 7 - unused (was for color paste)<br /> + 8 - Beep on message (alert_beep)<br /> + 9 - Blink Tray (alert_tray)<br /> + 10 - Blink Task Bar (alert_taskbar)<br /> +<p>Example of checking if the current context has Hide Join/Part messages set:</p> +<div class="example"> +<code> +if( Xchat::context_info->{flags} & (1 << 6) ) { + Xchat::print( "Hide Join/Part messages is enabled" ); +} +</code> +</div> + + </td> + </tr> + + <tr> + <td>id</td> <td>Unique server ID </td> + </tr> + + <tr> + <td>lag</td> + <td>lag in milliseconds</td> + </tr> + + <tr> + <td>maxmodes</td> <td>Maximum modes per line</td> + </tr> + + <tr> + <td>network</td> <td>network name to which this channel belongs</td> + </tr> + + <tr> + <td>nickprefixes</td> <td>Nickname prefixes e.g. "+@"</td> + </tr> + + <tr> + <td>nickmodes</td> <td>Nickname mode chars e.g. "vo"</td> + </tr> + + <tr> + <td>queue</td> + <td>number of bytes in the send queue</td> + </tr> + + <tr> + <td>server</td> <td>server name to which this channel belongs</td> + </tr> + + <tr> + <td>type</td> <td>the type of this context<br /> + 1 - server<br /> + 2 - channel<br /> + 3 - dialog<br /> + </td> + </tr> + + <tr> + <td>users</td> <td>Number of users in this channel</td> + </tr> +</table> + +<p>"dcc" - list of DCC file transfers</p> +<table border="1"> + + <tr style="background-color: #dddddd"> + <td>Key</td> <td>Value</td> + </tr> + + <tr> + <td>address32</td> <td>address of the remote user(ipv4 address)</td> + </tr> + + <tr> + <td>cps</td> <td>bytes per second(speed)</td> + </tr> + + <tr> + <td>destfile</td> <td>destination full pathname</td> + </tr> + + <tr> + <td>file</td> <td>file name</td> + </tr> + + <tr> + <td>nick</td> + <td>nick of the person this DCC connection is connected to</td> + </tr> + + <tr> + <td>port</td> <td>TCP port number</td> + </tr> + + <tr> + <td>pos</td> <td>bytes sent/received</td> + </tr> + + <tr> + <td>poshigh</td> <td>bytes sent/received, high order 32 bits</td> + </tr> + + <tr> + <td>resume</td> <td>point at which this file was resumed<br /> + (zero if it was not resumed) + </td> + </tr> + + <tr> + <td>resumehigh</td> <td>point at which this file was resumed, high order 32 bits<br /> + </td> + </tr> + + <tr> + <td>size</td> <td>file size in bytes low order 32 bits</td> + </tr> + + <tr> + <td>sizehigh</td> <td>file size in bytes, high order 32 bits (when the files is > 4GB)</td> + </tr> + <tr> + <td>status</td> <td>DCC Status:<br /> + 0 - queued<br /> + 1 - active<br /> + 2 - failed<br /> + 3 - done<br /> + 4 - connecting<br /> + 5 - aborted + </td> + </tr> + + <tr> + <td>type</td> <td>DCC Type:<br /> + 0 - send<br /> + 1 - receive<br /> + 2 - chatrecv<br /> + 3 - chatsend + </td> + </tr> + +</table> + +<p>"ignore" - current ignore list</p> +<table border="1"> + + <tr style="background-color: #dddddd"> + <td>Key</td> <td>Value</td> + </tr> + + <tr> + <td>mask</td> <td>ignore mask. e.g: *!*@*.aol.com</td> + </tr> + + <tr> + <td>flags</td> <td>Bit field of flags.<br /> + 0 - private<br /> + 1 - notice<br /> + 2 - channel<br /> + 3 - ctcp<br /> + 4 - invite<br /> + 5 - unignore<br /> + 6 - nosave<br /> + 7 - dcc<br /> + </td> + </tr> + +</table> + +<p>"notify" - list of people on notify</p> +<table border="1"> + <tr style="background-color: #dddddd"> + <td>Key</td> <td>Value</td> + </tr> + + <tr> + <td>networks</td> + <td>comma separated list of networks where you will be notfified about this user's online/offline status or undef if you will be notificed on every network you are connected to</td> + </tr> + + <tr> + <td>nick</td> <td>nickname</td> + </tr> + + <tr> + <td>flags</td> <td>0 = is online</td> + </tr> + + <tr> + <td>on</td> <td>time when user came online</td> + </tr> + + <tr> + <td>off</td> <td>time when user went offline</td> + </tr> + + <tr> + <td>seen</td> <td>time when user was last verified still online</td> + </tr> +</table> + +<p>the values indexed by on, off and seen can be passed to localtime +and gmtime, see perldoc -f <a href="http://perldoc.perl.org/functions/localtime.html">localtime</a> and perldoc -f <a href="http://perldoc.perl.org/functions/gmtime.html">gmtime</a> for more +detail</p> + +<p>"users" - list of users in the current channel</p> +<table border="1"> + + <tr style="background-color: #dddddd"> + <td>Key</td> <td>Value</td> + </tr> + + <tr> + <td>away</td> <td>away status(boolean)</td> + </tr> + + <tr> + <td>lasttalk</td> + <td>last time a user was seen talking, this is the an epoch time(number of seconds since a certain date, that date depends on the OS)</td> + </tr> + + <tr> + <td>nick</td> <td>nick name</td> + </tr> + + <tr> + <td>host</td> + <td>host name in the form: user@host or undef if not known</td> + </tr> + + <tr> + <td>prefix</td> <td>prefix character, .e.g: @ or +</td> + </tr> + + <tr> + <td>realname</td> + <td>Real name or undef</td> + </tr> + + <tr> + <td>selected</td> + <td>selected status in the user list, only works when retrieving the user list of the focused tab. You can use the /USELECT command to select the nicks</td> + </tr> +</table> + +<p>"networks" - list of networks and the associated settings from network list</p> +<table border="1"> + + <tr style="background-color: #dddddd"> + <td>Key</td> <td>Value</td> + </tr> + + <tr> + <td>autojoins</td> <td>An object with the following methods:<br /> + <table> + <tr> + <td>Method</td> + <td>Description</td> + </tr> + + <tr> + <td>channels()</td> + <td>returns a list of this networks' autojoin channels in list context, a count of the number autojoin channels in scalar context</td> + </tr> + + <tr> + <td>keys()</td> + <td>returns a list of the keys to go with the channels, the order is the same as the channels, if a channel doesn't have a key, '' will be returned in it's place</td> + </tr> + + <tr> + <td>pairs()</td> + <td>a combination of channels() and keys(), returns a list of (channels, keys) pairs. This can be assigned to a hash for a mapping from channel to key.</td> + </tr> + + <tr> + <td>as_hash()</td> + <td>return the pairs as a hash reference</td> + </tr> + + <tr> + <td>as_string()</td> + <td>the original string that was used to construct this autojoin object, this can be used with the JOIN command to join all the channels in the autojoin list</td> + </tr> + + <tr> + <td>as_array()</td> + <td>return an array reference of hash references consisting of the keys "channel" and "key"</td> + </tr> + + <tr> + <td>as_bool()</td> + <td>returns true if the network has autojoins and false otherwise</td> + </tr> + </table> + </td> + </tr> + + <tr> + <td>connect_commands</td> <td>An array reference containing the connect commands for a network. An empty array if there aren't any</td> + </tr> + + <tr> + <td>encoding</td> <td>the encoding for the network</td> + </tr> + + + <tr> + <td>flags</td> + <td> + a hash reference corresponding to the checkboxes in the network edit window + <table> + <tr> + <td>allow_invalid</td> + <td>true if "Accept invalid SSL certificate" is checked</td> + </tr> + + <tr> + <td>autoconnect</td> + <td>true if "Auto connect to this network at startup" is checked</td> + </tr> + + <tr> + <td>cycle</td> + <td>true if "Connect to selected server only" is <strong>NOT</strong> checked</td> + </tr> + + <tr> + <td>use_global</td> + <td>true if "Use global user information" is checked</td> + </tr> + + <tr> + <td>use_proxy</td> + <td>true if "Bypass proxy server" is <strong>NOT</strong> checked</td> + </tr> + + <tr> + <td>use_ssl</td> + <td>true if "Use SSL for all the servers on this network" is checked</td> + </tr> + </table> + </td> + </tr> + + + <tr> + <td>irc_nick1</td> + <td>Corresponds with the "Nick name" field in the network edit window</td> + </tr> + + <tr> + <td>irc_nick2</td> + <td>Corresponds with the "Second choice" field in the network edit window</td> + </tr> + + <tr> + <td>irc_real_name</td> + <td>Corresponds with the "Real name" field in the network edit window</td> + </tr> + + + <tr> + <td>irc_user_name</td> + <td>Corresponds with the "User name" field in the network edit window</td> + </tr> + + + <tr> + <td>network</td> + <td>Name of the network</td> + </tr> + + <tr> + <td>nickserv_password</td> + <td>Corresponds with the "Nickserv password" field in the network edit window</td> + </tr> + + <tr> + <td>selected</td> + <td>Index into the list of servers in the "servers" key, this is used if the "cycle" flag is false</td> + </tr> + + <tr> + <td>server_password</td> + <td>Corresponds with the "Server password" field in the network edit window</td> + </tr> + + <tr> + <td>servers</td> + <td>An array reference of hash references with a "host" and "port" key. If a port is not specified then 6667 will be used.</td> + </tr> +</table> + +=end html + +=head3 C<Xchat::user_info( [$nick] )> + +=over 3 + +=item * +C<$nick> - the nick to look for, if this is not given your own nick will be + used as default + +=back + +This function is mainly intended to be used as a shortcut for when you need +to retrieve some information about only one user in a channel. Otherwise it +is better to use L<get_list|xchat_get_list>. +If C<$nick> is found a hash reference containing the same keys as those in the +"users" list of L<get_list|xchat_get_list> is returned otherwise undef is returned. +Since it relies on L<get_list|xchat_get_list> this function can only be used in a +channel context. + +=head3 C<Xchat::context_info( [$context] )> + +=over 3 + +=item * +C<$context> - context returned from L<get_context|xchat_get_context>, L<find_context|xchat_find_context> and L<get_list|xchat_get_list>, this is the context that you want infomation about. If this is omitted, it will default to current context. + +=back + +This function will return the information normally retrieved with L<get_info|xchat_get_info>, except this is for the context that is passed in. The information will be returned in the form of a hash. The keys of the hash are the C<$id> you would normally supply to L<get_info|xchat_get_info> as well as all the keys that are valid for the items in the "channels" list from L<get_list|xchat_get_list>. Use of this function is more efficient than calling get_list( "channels" ) and searching through the result. + +=begin html + +<p>Example:</p> +<div class="example"> +use strict; +use warnings; +use Xchat qw(:all); # imports all the functions documented on this page + +register( "User Count", "0.1", + "Print out the number of users on the current channel" ); + +hook_command( "UCOUNT", \&display_count ); + +sub display_count { + prnt "There are " . context_info()->{users} . " users in this channel."; + return EAT_XCHAT; +} +</div> + +=end html + +=head3 C<Xchat::strip_code( $string )> + +=over 3 + +=item * +C<$string> - string to remove codes from + +=back + +This function will remove bold, color, beep, reset, reverse and underline codes from C<$string>. It will also remove ANSI escape codes which might get used by certain terminal based clients. If it is called in void context C<$string> will be modified otherwise a modified copy of C<$string> is returned. + +=head2 Examples + +=head3 Asynchronous DNS resolution with hook_fd + +=begin html + +<div class="example"> +<code> +use strict; +use warnings; +use Xchat qw(:all); +use Net::DNS; + +hook_command( "BGDNS", sub { + my $host = $_[0][1]; + my $resolver = Net::DNS::Resolver->new; + my $sock = $resolver->bgsend( $host ); + + hook_fd( $sock, sub { + my $ready_sock = $_[0]; + my $packet = $resolver->bgread( $ready_sock ); + + if( $packet->authority && (my @answers = $packet->answer ) ) { + + if( @answers ) { + prnt "$host:"; + my $padding = " " x (length( $host ) + 2); + for my $answer ( @answers ) { + prnt $padding . $answer->rdatastr . ' ' . $answer->type; + } + } + } else { + prnt "Unable to resolve $host"; + } + + return REMOVE; + }, + { + flags => FD_READ, + }); + + return EAT_XCHAT; +}); +</code> +</div> + +=end html + +=head2 Contact Information + +Contact Lian Wan Situ at E<lt>atmcmnky [at] yahoo.comE<gt> for questions, comments and +corrections about this page or the Perl plugin itself. You can also find me +in #xchat on FreeNode under the nick Khisanth. diff --git a/plugins/perl/lib/Xchat/Embed.pm b/plugins/perl/lib/Xchat/Embed.pm new file mode 100644 index 00000000..dffbaf5e --- /dev/null +++ b/plugins/perl/lib/Xchat/Embed.pm @@ -0,0 +1,253 @@ +package Xchat::Embed; +use strict; +use warnings; +# list of loaded scripts keyed by their package names +our %scripts; + +sub load { + my $file = expand_homedir( shift @_ ); + my $package = file2pkg( $file ); + + if( exists $scripts{$package} ) { + my $pkg_info = pkg_info( $package ); + my $filename = File::Basename::basename( $pkg_info->{filename} ); + Xchat::printf( + qq{'%s' already loaded from '%s'.\n}, + $filename, $pkg_info->{filename} + ); + Xchat::print( + 'If this is a different script then it rename and try '. + 'loading it again.' + ); + return 2; + } + + if( open my $source_handle, $file ) { + my $source = do {local $/; <$source_handle>}; + close $source_handle; + # we shouldn't care about things after __END__ + $source =~ s/^__END__.*//ms; + + if( + my @replacements = $source =~ + m/^\s*package ((?:[^\W:]+(?:::)?)+)\s*?;/mg + ) { + + if ( @replacements > 1 ) { + Xchat::print( + "Too many package defintions, only 1 is allowed\n" + ); + return 1; + } + + my $original_package = shift @replacements; + + # remove original package declaration + $source =~ s/^(package $original_package\s*;)/#$1/m; + + # fixes things up for code calling subs with fully qualified names + $source =~ s/${original_package}:://g; + } + + # this must come before the eval or the filename will not be found in + # Xchat::register + $scripts{$package}{filename} = $file; + $scripts{$package}{loaded_at} = Time::HiRes::time(); + + my $full_path = File::Spec->rel2abs( $file ); + $source =~ s/^/#line 1 "$full_path"\n\x7Bpackage $package;/; + + # make sure we add the closing } even if the last line is a comment + if( $source =~ /^#.*\Z/m ) { + $source =~ s/^(?=#.*\Z)/\x7D/m; + } else { + $source =~ s/\Z/\x7D/; + } + + _do_eval( $source ); + + unless( exists $scripts{$package}{gui_entry} ) { + $scripts{$package}{gui_entry} = + Xchat::Internal::register( + "", "unknown", "", $file + ); + } + + if( $@ ) { + # something went wrong + $@ =~ s/\(eval \d+\)/$file/g; + Xchat::print( "Error loading '$file':\n$@\n" ); + # make sure the script list doesn't contain false information + unload( $scripts{$package}{filename} ); + return 1; + } + } else { + Xchat::print( "Error opening '$file': $!\n" ); + return 2; + } + + return 0; +} + +sub _do_eval { + no strict; + no warnings; + eval $_[0]; +} + +sub unload { + my $file = shift @_; + my $package = file2pkg( $file ); + my $pkg_info = pkg_info( $package ); + + if( $pkg_info ) { + # take care of the shutdown callback + if( exists $pkg_info->{shutdown} ) { + # allow incorrectly written scripts to be unloaded + eval { + if( ref $pkg_info->{shutdown} eq 'CODE' ) { + $pkg_info->{shutdown}->(); + } elsif ( $pkg_info->{shutdown} ) { + no strict 'refs'; + &{$pkg_info->{shutdown}}; + } + }; + } + + if( exists $pkg_info->{hooks} ) { + for my $hook ( @{$pkg_info->{hooks}} ) { + Xchat::unhook( $hook, $package ); + } + } + + + if( exists $pkg_info->{gui_entry} ) { + plugingui_remove( $pkg_info->{gui_entry} ); + } + + Symbol::delete_package( $package ); + delete $scripts{$package}; + return Xchat::EAT_ALL; + } else { + Xchat::print( qq{"$file" is not loaded.\n} ); + return Xchat::EAT_NONE; + } +} + +sub unload_all { + for my $package ( keys %scripts ) { + unload( $scripts{$package}->{filename} ); + } + + return Xchat::EAT_ALL; +} + +sub reload { + my $file = shift @_; + my $package = file2pkg( $file ); + my $pkg_info = pkg_info( $package ); + my $fullpath = $file; + + if( $pkg_info ) { + $fullpath = $pkg_info->{filename}; + unload( $file ); + } + + load( $fullpath ); + return Xchat::EAT_ALL; +} + +sub reload_all { + my @dirs = Xchat::get_info( "xchatdirfs" ) || Xchat::get_info( "xchatdir" ); + push @dirs, File::Spec->catdir( $dirs[0], "plugins" ); + for my $dir ( @dirs ) { + my $auto_load_glob = File::Spec->catfile( $dir, "*.pl" ); + my @scripts = map { $_->{filename} } + sort { $a->{loaded_at} <=> $b->{loaded_at} } values %scripts; + push @scripts, File::Glob::bsd_glob( $auto_load_glob ); + + my %seen; + @scripts = grep { !$seen{ $_ }++ } @scripts; + + unload_all(); + for my $script ( @scripts ) { + if( !pkg_info( file2pkg( $script ) ) ) { + load( $script ); + } + } + } +} + +sub expand_homedir { + my $file = shift @_; + + if ( $^O eq "MSWin32" ) { + $file =~ s/^~/$ENV{USERPROFILE}/; + } else { + $file =~ s{^~}{ + (getpwuid($>))[7] || $ENV{HOME} || $ENV{LOGDIR} + }ex; + } + return $file; +} + +sub file2pkg { + my $string = File::Basename::basename( shift @_ ); + $string =~ s/\.pl$//i; + $string =~ s|([^A-Za-z0-9/])|'_'.unpack("H*",$1)|eg; + return "Xchat::Script::" . $string; +} + +sub pkg_info { + my $package = shift @_; + return $scripts{$package}; +} + +sub find_external_pkg { + my $level = 1; + + while( my @frame = caller( $level ) ) { + return @frame if $frame[0] !~ /^Xchat/; + $level++; + } + +} + +sub find_pkg { + my $level = 1; + + while( my ($package, $file, $line) = caller( $level ) ) { + return $package if $package =~ /^Xchat::Script::/; + $level++; + } + + my @frame = find_external_pkg(); + my $location; + + if( $frame[0] or $frame[1] ) { + $location = $frame[1] ? $frame[1] : "package $frame[0]"; + $location .= " line $frame[2]"; + } else { + $location = "unknown location"; + } + + die "Unable to determine which script this hook belongs to. at $location\n"; + +} + +sub fix_callback { + my ($package, $callback) = @_; + + unless( ref $callback ) { + # change the package to the correct one in case it was hardcoded + $callback =~ s/^.*:://; + $callback = qq[${package}::$callback]; + + no strict 'subs'; + $callback = \&{$callback}; + } + + return $callback; +} + +1 diff --git a/plugins/perl/lib/Xchat/List/Network.pm b/plugins/perl/lib/Xchat/List/Network.pm new file mode 100644 index 00000000..da2f52dd --- /dev/null +++ b/plugins/perl/lib/Xchat/List/Network.pm @@ -0,0 +1,32 @@ +package Xchat::List::Network; +use strict; +use warnings; +use Storable qw(dclone); +my $last_modified; +my @servers; + +sub get { + my $server_file = Xchat::get_info( "xchatdirfs" ) . "/servlist_.conf"; + + # recreate the list only if the server list file has changed + if( -f $server_file && + (!defined $last_modified || $last_modified != -M $server_file ) ) { + $last_modified = -M _; + + if( open my $fh, "<", $server_file ) { + local $/ = "\n\n"; + while( my $record = <$fh> ) { + chomp $record; + next if $record =~ /^v=/; # skip the version line + push @servers, Xchat::List::Network::Entry::parse( $record ); + } + } else { + warn "Unable to open '$server_file': $!"; + } + } + + my $clone = dclone( \@servers ); + return @$clone; +} + +1 diff --git a/plugins/perl/lib/Xchat/List/Network/AutoJoin.pm b/plugins/perl/lib/Xchat/List/Network/AutoJoin.pm new file mode 100644 index 00000000..16036a9d --- /dev/null +++ b/plugins/perl/lib/Xchat/List/Network/AutoJoin.pm @@ -0,0 +1,82 @@ +package Xchat::List::Network::AutoJoin; +use strict; +use warnings; + +use overload +# '%{}' => \&as_hash, +# '@{}' => \&as_array, + '""' => 'as_string', + '0+' => 'as_bool'; + +sub new { + my $class = shift; + my $line = shift; + + my @autojoins; + + if ( $line ) { + my ( $channels, $keys ) = split / /, $line, 2; + my @channels = split /,/, $channels; + my @keys = split /,/, ($keys || ''); + + for my $channel ( @channels ) { + my $key = shift @keys; + $key = '' unless defined $key; + + push @autojoins, { + channel => $channel, + key => $key, + }; + } + } + return bless \@autojoins, $class; +} + +sub channels { + my $self = shift; + + if( wantarray ) { + return map { $_->{channel} } @$self; + } else { + return scalar @$self; + } +} + +sub keys { + my $self = shift; + return map { $_->{key} } @$self ; + +} + +sub pairs { + my $self = shift; + + my @channels = $self->channels; + my @keys = $self->keys; + + my @pairs = map { $_ => shift @keys } @channels; +} + +sub as_hash { + my $self = shift; + return +{ $self->pairs }; +} + +sub as_string { + my $self = shift; + return join " ", + join( ",", $self->channels ), + join( ",", $self->keys ); +} + +sub as_array { + my $self = shift; + return [ map { \%$_ } @$self ]; +} + +sub as_bool { + my $self = shift; + return $self->channels ? 1 : ""; +} + +1 diff --git a/plugins/perl/lib/Xchat/List/Network/Entry.pm b/plugins/perl/lib/Xchat/List/Network/Entry.pm new file mode 100644 index 00000000..e40b48bd --- /dev/null +++ b/plugins/perl/lib/Xchat/List/Network/Entry.pm @@ -0,0 +1,105 @@ +package Xchat::List::Network::Entry; +use strict; +use warnings; + +my %key_for = ( + I => "irc_nick1", + i => "irc_nick2", + U => "irc_user_name", + R => "irc_real_name", + P => "server_password", + B => "nickserv_password", + N => "network", + D => "selected", + E => "encoding", +); +my $letter_key_re = join "|", keys %key_for; + +sub parse { + my $data = shift; + my $entry = { + irc_nick1 => undef, + irc_nick2 => undef, + irc_user_name => undef, + irc_real_name => undef, + server_password => undef, + + # the order of the channels need to be maintained + # list of { channel => .., key => ... } + autojoins => Xchat::List::Network::AutoJoin->new( '' ), + connect_commands => [], + flags => {}, + selected => undef, + encoding => undef, + servers => [], + nickserv_password => undef, + network => undef, + }; + + my @fields = split /\n/, $data; + chomp @fields; + + for my $field ( @fields ) { + SWITCH: for ( $field ) { + /^($letter_key_re)=(.*)/ && do { + $entry->{ $key_for{ $1 } } = $2; + last SWITCH; + }; + + /^J.(.*)/ && do { + $entry->{ autojoins } = + Xchat::List::Network::AutoJoin->new( $1 ); + }; + + /^F.(.*)/ && do { + $entry->{ flags } = parse_flags( $1 ); + }; + + /^S.(.+)/ && do { + push @{$entry->{servers}}, parse_server( $1 ); + }; + + /^C.(.+)/ && do { + push @{$entry->{connect_commands}}, $1; + }; + } + } + +# $entry->{ autojoins } = $entry->{ autojoin_channels }; + return $entry; +} + +sub parse_flags { + my $value = shift || 0; + my %flags; + + $flags{ "cycle" } = $value & 1 ? 1 : 0; + $flags{ "use_global" } = $value & 2 ? 1 : 0; + $flags{ "use_ssl" } = $value & 4 ? 1 : 0; + $flags{ "autoconnect" } = $value & 8 ? 1 : 0; + $flags{ "use_proxy" } = $value & 16 ? 1 : 0; + $flags{ "allow_invalid" } = $value & 32 ? 1 : 0; + + return \%flags; +} + +sub parse_server { + my $data = shift; + if( $data ) { + my ($host, $port) = split /\//, $data; + unless( $port ) { + my @parts = split /:/, $host; + + # if more than 2 then we are probably dealing with a IPv6 address + # if less than 2 then no port was specified + if( @parts == 2 ) { + $port = $parts[1]; + } + } + + $port ||= 6667; + return { host => $host, port => $port }; + } +} + +1 |