#! /usr/bin/perl -w # click-elem2man -- creates man pages from structured comments in element # source code # Eddie Kohler # Robert Morris - original make-faction-html script # # Copyright (c) 1999-2001 Massachusetts Institute of Technology # Copyright (c) 2001-2003 International Computer Science Institute # Copyright (c) 2006 Regents of the University of California # # Permission is hereby granted, free of charge, to any person obtaining a # copy of this software and associated documentation files (the "Software"), # to deal in the Software without restriction, subject to the conditions # listed in the Click LICENSE file. These conditions include: you must # preserve this copyright notice, and you cannot mention the copyright # holders in advertising related to the Software without their permission. # The Software is provided WITHOUT ANY WARRANTY, EXPRESS OR IMPLIED. This # notice is a summary of the Click LICENSE file; the license in that file is # legally binding. # information stored about each Click element, indexed by C++ class name my(%processing, %deprecated, %portcount, %docrequires, %requires, $PREFIX); my(%section_break) = ( 'head1' => 1, 'c' => 1, 's' => 1, 'io' => 1, 'processing' => 1, 'drivers' => 1, 'd' => 1, 'n' => 1, 'e' => 1, 'h' => 1, 'a' => 1, 'title' => 1, 'deprecated' => 1, 'package' => 1 ); my(%section_takes_args) = ( 'head1' => 1, 'c' => 0, 's' => 2, 'io' => 2, 'processing' => 2, 'drivers' => 2, 'd' => 0, 'n' => 0, 'e' => 0, 'h' => 1, 'a' => 2, 'title' => 1, 'deprecated' => 1, 'package' => 2 ); my(%section_takes_text) = ( 'head1' => 2, 'c' => 1, 's' => 1, 'io' => 2, 'processing' => 2, 'drivers' => 2, 'd' => 1, 'n' => 1, 'e' => 1, 'h' => 1, 'a' => 2, 'title' => 0, 'deprecated' => 0, 'package' => 1 ); my(%xsection_takes_args) = ( 'head1' => 1, 'head2' => 1, 'item' => 1, 'over' => 1, 'back' => 0, 'for' => 1, 'begin' => 1, 'end' => 1, 'start!' => 1, 'name!' => 1, 'end!' => 0, 'text' => 0 ); my(%infosection_name) = ( 'package' => 'Package', 'io' => 'Ports', 'processing' => 'Processing', 'drivers' => 'Drivers' ); my $section = 'n'; my $package; my $uninstall = 0; my(@all_outnames, %all_outsections, %summaries_by_section, %all_roff_summaries); my(%processing_constants) = ( 'AGNOSTIC' => 'a/a', 'PUSH' => 'h/h', 'PULL' => 'l/l', 'PUSH_TO_PULL' => 'h/l', 'PULL_TO_PUSH' => 'l/h' ); my(%processing_text) = ( 'a/a' => 'agnostic', 'h/h' => 'push', 'l/l' => 'pull', 'h/l' => 'push inputs, pull outputs', 'l/h' => 'pull inputs, push outputs', 'a/ah' => 'agnostic, but output 1 is push' ); my(@section_headers) = ( 'basicsources' => 'Basic Sources and Sinks', 'classification' => 'Basic Classification and Selection', 'basictransfer' => 'Basic Packet Transfer', 'counters' => 'Counters', 'timestamps' => 'Timestamps', 'basicmod' => 'Basic Packet Modification', 'storage' => 'Packet Storage', 'aqm' => 'Active Queue Management', 'scheduling' => 'Packet Scheduling', 'shaping' => 'Traffic Shaping', 'information' => 'Information Elements', 'netdevices' => 'Network Devices', 'comm' => 'Host and Socket Communication', 'ethernet' => 'Ethernet', 'arp' => 'ARP', 'ip' => 'IPv4', 'iproute' => 'IPv4 Routing', 'icmp' => 'ICMP', 'nat' => 'Network Address Translation', 'tcp' => 'TCP', 'udp' => 'UDP', 'app' => 'Applications', 'traces' => 'Trace Manipulation', 'ipmeasure' => 'TCP/IP Measurement', 'aggregates' => 'Aggregates', 'ip6' => 'IPv6', 'ipsec' => 'IPsec', 'crc' => 'CRCs', 'paint' => 'Paint Annotations', 'annotations' => 'Annotations', 'debugging' => 'Debugging', 'control' => 'Control', 'smpclick' => 'Multithreaded Click', 'test' => 'Regression Tests' ); my(%section_headers); for (my $i = 0; $i < @section_headers; $i += 2) { $section_headers{$section_headers[$i]} = $section_headers[$i+1]; } my(%default_packages) = ( 'analysis' => 'analysis (core)', 'app' => 'app (core)', 'aqm' => 'aqm (core)', 'bsdmodule' => 'bsdmodule (core)', 'ethernet' => 'ethernet (core)', 'etherswitch' => 'etherswitch (core)', 'grid' => 'grid (core)', 'icmp' => 'icmp (core)', 'ip' => 'ip (core)', 'ip6' => 'ip6 (core)', 'ipsec' => 'ipsec (core)', 'linuxmodule' => 'linuxmodule (core)', 'local' => 'local (core)', 'ns' => 'ns (core)', 'radio' => 'radio (core)', 'simple' => 'simple (core)', 'standard' => 'standard (core)', 'tcpudp' => 'tcpudp (core)', 'test' => 'test (core)', 'userlevel' => 'userlevel (core)', 'wifi' => 'wifi (core)' ); # find date my($today) = ''; if (localtime =~ /\w*\s+(\w*)\s+(\d*)\s+\S*\s+(\d*)/) { $today = "$2/$1/$3"; } # Unrolling [^A-Z>]|[A-Z](?!<) gives: // MRE pp 165. my $nonest = '(?:[^A-Z>]*(?:[A-Z](?!<)[^A-Z>]*)*)'; # XXX one paragraph at a time my($Filename, $PrimaryElement, @Related, %RelatedSource, @Over, $Begun); ############ ## shared ## sub textize_add_links ($$) { my($t, $selfref) = @_; my($pos); # embolden & manpageize foreach my $r (@Related) { my $l = length($r); my $result = ($r eq $PrimaryElement ? "$selfref<$r>" : "L<$r>"); for ($pos = index($t, $r); $pos >= 0; $pos = index($t, $r, $pos + 1)) { if (($pos == 0 || substr($t, $pos - 1, 1) =~ /^[^\w@\/<]$/s) && (substr($t, $pos + $l, 1) =~ /^[^\w@\/]$/s)) { substr($t, $pos, $l) = $result; $pos += 2 + $l; } } } $t; } sub quote_unquoted_gt ($) { my($t) = @_; my($tt, $nest, $pos) = ('', 0); for ($pos = index($t, ">"); $pos >= 0; $pos = index($t, ">")) { my($w) = substr($t, 0, $pos); $nest++ while $w =~ m|[A-Z]<|g; if ($nest) { $tt .= $w . ">"; $nest--; } else { $tt .= $w . "E"; } $t = substr($t, $pos + 1); } $tt . $t; } ########### ## nroff ## my $nroff_prologue = <<'EOD;'; .de M .IR "\\$1" "(\\$2)\\$3" .. .de RM .RI "\\$1" "\\$2" "(\\$3)\\$4" .. EOD; chomp $nroff_prologue; my(%nroff_podentities) = ( 'lt' => '<', 'gt' => '>', 'amp' => '&', 'solid' => '/', 'verbar' => '|', 'eq' => '=', 'star' => '*', 'lparen' => '(', 'rparen' => ')', 'lbrack' => '[', 'rbrack' => ']', 'lbrace' => '{', 'rbrace' => '}' ); sub nroff_quote ($) { my($x) = @_; $x =~ tr/\000-\177/\200-\377/; $x; } sub nroff_unentity ($) { my($x) = @_; $x =~ tr/\200-\377/\000-\177/; if ($x =~ /^\d+$/) { chr($x); } elsif ($nroff_podentities{$x}) { $nroff_podentities{$x}; } else { print STDERR "click-elem2man: $Filename: unknown entity E<$x>\n"; ""; } } sub nroff_fixfP ($$) { my($x, $f) = @_; $x =~ s/\\fP/\\f$f/g; $x; } sub nroffize_text ($) { my($t) = @_; my($s); # embolden & manpageize $t = textize_add_links($t, 'L'); $t =~ s/\\/\\\\/g; $t =~ s/^\./\\&./gm; $t =~ s/^'/\\&'/gm; $t =~ s/^\s*$/.PP\n/gm; $t =~ s/^\.PP\n(?:\.PP\n)+/.PP\n/gm; $t =~ s{\A\s*(?:\.PP\n)?}{}s; if (@Over > 0) { $t =~ s/^\.PP$/.IP \"\" $Over[-1]/gm; } # get rid of entities $t =~ s{[\200-\377]}{"E<" . ord($1) . ">"}ge; $t =~ s{(E<[^>]*>)}{nroff_quote($1)}ge; my $maxnest = 10; while ($maxnest-- && $t =~ /[A-Z]/"\\f$1" . nroff_fixfP($2, $1) . "\\fP"/eg; $t =~ s/[PU]<($nonest)>/$1/g; # files and filelike refs in italics $t =~ s/F<($nonest)>/I<$1>/g; # LREF: man page references $t =~ s{L<($nonest)\(([\dln])\)>(\S*)(\s*)}{\n.M $1 $2 $3\n}g; $t =~ s/V<($nonest)>//g; # LREF: a la HREF L $t =~ s{L<($nonest)>(\S*)(\s*)}{ if (($s = $RelatedSource{$1})) { "\n.M $1 \"$s\" $2\n"; } else { my $i = index($1, "|"); ($i >= 0 ? substr($1, 0, $i) . $2 . $3 : "\\fB$1\\fP$2$3"); } }eg; $t =~ s/Z<>/\\&/g; $t =~ s/N<>(\n?)/\n.br\n/g; # comes last because not subject to reprocessing $t =~ s/C<($nonest)>/"\\f(CW" . nroff_fixfP($1, 'R') . "\\fP"/eg; } # replace entities $t =~ s/\305\274([^\276]*)\276/nroff_unentity($1)/ge; # fix fonts $t =~ s/\\fP/\\fR/g; # fix manual $t =~ s/\n\.M (\S+) \"(\S+)\" (\(\2\))/\n.M $1 \"$2\" /sg; $t =~ s/\n+/\n/sg; $t =~ s/^\n+//; $t =~ s/\n+$//; $t; } sub nroffize ($) { my($t) = @_; # $t cannot contain \t, as all \ts have been expanded to spaces below #$t =~ s{\n[ \r]+$}{\n}gm; if ($t =~ /^ /m) { # worry about verbatims $t =~ s/^( .*)\n(\n+)(?= )/$1 . "\n" . (" \n" x length($2))/mge; my(@x) = split(/(^ .*$)/m, $t); my($o, $i) = ''; for ($i = 0; $i < @x; $i += 2) { if ($x[$i]) { $o .= nroffize_text($x[$i]) . "\n"; } if ($x[$i+1]) { $x[$i+1] =~ s/\\/\\e/g; $o .= ".nf\n\\&" . $x[$i+1] . "\n.fi\n.PP\n"; } } $o =~ s/\n\.fi\n\.PP\n\n\.nf\n/\n/g; if (@Over > 0) { $o =~ s/^\.PP$/.IP \"\" $Over[-1]/gm; } $o; } else { nroffize_text($t); } } sub nroff_do_section ($$$) { my($name, $args, $text) = @_; if (!exists($xsection_takes_args{$name})) { print STDERR "click-elem2man: $Filename: unknown section '=$name' ignored\n"; return; } print STDERR "click-elem2man: $Filename: section '=$name' requires arguments\n" if ($xsection_takes_args{$name} && !$args); # handle '=begin' .. '=end' if ($name eq 'end') { undef $Begun; } elsif ($Begun && ($Begun eq 'man' || $Begun eq 'roff')) { print OUT '=', $name, ($args ? ' ' . $args : ''), "\n", $text; return; } elsif ($Begun) { return; } if ($name eq 'head1') { print OUT ".SH \"", nroffize($args), "\"\n"; } elsif ($name eq 'head2') { print OUT ".SS \"", nroffize($args), "\"\n"; } elsif ($name eq 'over') { if ($args =~ /^\s*(\d+)\s*$/s) { print OUT ".RS $Over[-1]\n" if @Over; push @Over, $1; } else { print STDERR "click-elem2man: $Filename: bad arguments to '=over'\n"; } } elsif ($name eq 'item') { if (@Over == 0) { print STDERR "click-elem2man: $Filename: '=item' outside any '=over' section\n"; } else { print OUT ".IP \"", nroffize($args), "\" $Over[-1]\n"; } } elsif ($name eq 'back') { my($overarg); if ($args =~ /^\s*(\d+)\s*$/s) { $overarg = $1; } elsif ($args !~ /^\s*$/s) { print STDERR "click-elem2man: $Filename: bad arguments to '=back'\n"; } if (@Over == 0) { print STDERR "click-elem2man: $Filename: too many '=back's\n"; } else { my($over) = pop @Over; print OUT ".RE\n" if @Over; print OUT (@Over ? ".IP \"\" $Over[-1]\n" : ".PP\n"); print STDERR "click-elem2man: $Filename: '=back $overarg' paired with '=over $over'\n" if defined($overarg) && $over != $overarg; } } elsif ($name eq 'for') { my($fortext); if ($text =~ /^(.*)\n\s*\n(.*)$/s) { ($fortext, $text) = ($1, $2); } else { ($fortext, $text) = ($text, ''); } if ($args =~ /^\s*(man|roff)\s*(.*)/) { print OUT $2, $fortext; } } elsif ($name eq 'begin') { $Begun = $args; $Begun =~ s/^\s*(\S+).*/$1/; if ($Begun eq 'man' || $Begun eq 'roff') { print OUT $text; } return; } elsif ($name eq 'start!') { print OUT <<"EOD;"; .\\" -*- mode: nroff -*- .\\" Generated by 'click-elem2man' from '$Filename' $nroff_prologue .TH "\U$text\E" $args "$today" "Click" EOD; return; } elsif ($name eq 'name!') { print OUT <<"EOD;"; .SH "NAME" $args \\- $text EOD; return; } elsif ($name eq 'end!') { return; } print OUT nroffize($text), "\n"; print OUT "\n" if $name eq 'head1'; } ############## ## dokuwiki ## my($dokuwiki_dl); my(%dokuwiki_podentities) = ( 'lt' => '<', 'gt' => "\xFF\xFF", 'amp' => '&', 'solid' => '/', 'verbar' => '|', 'eq' => '=', 'star' => '*', 'lparen' => '(', 'rparen' => ')', 'lbrack' => '[', 'rbrack' => ']', 'lbrace' => '{', 'rbrace' => '}' ); my($dokuwiki_sensitive_char) = "[!%'\\(\\)\\*\\+/?\\[\\\\\\]_\\{\\}]"; sub dokuwiki_unentity ($) { my($x) = @_; if ($x =~ /^\d+$/) { $x = pack 'U', $x; if ($x =~ /\A$dokuwiki_sensitive_char\Z/) { "%%$x%%"; } else { $x; } } elsif ($dokuwiki_podentities{$x}) { $dokuwiki_podentities{$x}; } else { print STDERR "click-elem2man: $Filename: unknown entity E<$x>\n"; ""; } } sub dokuwiki_quotedbl ($) { my($x) = @_; $x =~ s/^(.)/\%\%$1\%\%/; $x; } sub dokuwiki_surround ($$) { my($text, $chr) = @_; my($re) = "\\$chr\\$chr"; $text =~ s{$re}{}g; "$chr$chr$text$chr$chr"; } sub dokuwikiize_text ($) { my($t) = @_; # embolden & manpageize $t = textize_add_links($t, 'P'); # get rid of entities $t =~ s{E<([^>]*)>}{dokuwiki_unentity($1)}ge; $t =~ s{($dokuwiki_sensitive_char)}{^$1^}g; my $maxnest = 10; while ($maxnest-- && $t =~ /[A-Z]/dokuwiki_surround($1, "*")/eg; $t =~ s.I<($nonest)>.dokuwiki_surround($1, "/").eg; $t =~ s.U<($nonest)>.dokuwiki_surround($1, "_").eg; $t =~ s/P<($nonest)>/$1/g; $t =~ s/C<($nonest)>/dokuwiki_surround($1, "'")/eg; # files and filelike refs in italics $t =~ s/F<($nonest)>/I<$1>/g; # LREF: man page references $t =~ s{L<($nonest)\^\(\^[\dln]\^\)\^>}{[[$1]]}g; $t =~ s{L<($nonest)>\^\(\^[\dln]\^\)\^}{[[$1]]}g; # LREF: a la HREF L $t =~ s{L<($nonest)\|($nonest)>}{[[$2|$1]]}g; $t =~ s{L<($nonest)>}{[[$1]]}g; $t =~ s/V<($nonest)>//g; $t =~ s/Z<>//g; $t =~ s/N<>(\n?)/\\\\ /g; } $t =~ s/\n+/\n/sg; $t =~ s/^\n+//; $t =~ s/\n+$//; $t =~ s/\xFF\xFF/>/g; $t =~ s{^(\s*)\^([?!])\^}{$1 . dokuwiki_quotedbl($2)}egm; $t =~ s{($dokuwiki_sensitive_char)\^\1\^}{$1 . dokuwiki_quotedbl($1)}eg; $t =~ s{\^($dokuwiki_sensitive_char)\^\1}{dokuwiki_quotedbl($1) . $1}eg; $t =~ s{\^($dokuwiki_sensitive_char)\^}{$1}g; # remove self references $t =~ s{\[\[$PrimaryElement\]\]}{$PrimaryElement}g; $t; } sub dokuwikiize ($) { my($t) = @_; # $t cannot contain \t, as all \ts have been expanded to spaces below if ($t =~ /^ /m) { # worry about verbatims $t =~ s/^( .*)\n(\n+)(?= )/$1 . "\n" . (" \n" x length($2))/mge; my(@x) = split(/(^ .*$)/m, $t); my($o, $i) = ''; for ($i = 0; $i < @x; $i += 2) { if ($x[$i]) { $o .= dokuwikiize_text($x[$i]); $o .= "\n" if $o !~ /\n\Z/; } if ($x[$i+1]) { $o .= " " . $x[$i+1] . "\n"; } } $o; } else { dokuwikiize_text($t); } } sub dokuwiki_do_section ($$$) { my($name, $args, $text) = @_; if (!exists($xsection_takes_args{$name})) { print STDERR "click-elem2man: $Filename: unknown section '=$name' ignored\n"; return; } print STDERR "click-elem2man: $Filename: section '=$name' requires arguments\n" if ($xsection_takes_args{$name} && !$args); # handle '=begin' .. '=end' if ($name eq 'end') { undef $Begun; } elsif ($Begun && $Begun eq 'dokuwiki') { print OUT '=', $name, ($args ? ' ' . $args : ''), "\n", $text; return; } elsif ($Begun) { return; } if ($name eq 'head1') { print OUT "\n===== ", dokuwikiize($args), " =====\n\n"; } elsif ($name eq 'head2') { print OUT "\n==== ", dokuwikiize($args), " ====\n\n"; } elsif ($name eq 'over') { if ($args =~ /^\s*(\d+)\s*$/s) { push @Over, $1; } else { print STDERR "click-elem2man: $Filename: bad arguments to '=over'\n"; } } elsif ($name eq 'item') { if (@Over == 0) { print STDERR "click-elem2man: $Filename: '=item' outside any '=over' section\n"; } else { print OUT "\n", ' ' x @Over, "? " if $dokuwiki_dl; print OUT dokuwikiize("B<" . quote_unquoted_gt($args) . ">"); print OUT ($dokuwiki_dl ? "\n" : "\n\n"); } } elsif ($name eq 'back') { my($overarg); if ($args =~ /^\s*(\d+)\s*$/s) { $overarg = $1; } elsif ($args !~ /^\s*$/s) { print STDERR "click-elem2man: $Filename: bad arguments to '=back'\n"; } if (@Over == 0) { print STDERR "click-elem2man: $Filename: too many '=back's\n"; } else { my($over) = pop @Over; print STDERR "click-elem2man: $Filename: '=back $overarg' paired with '=over $over'\n" if defined($overarg) && $over != $overarg; } } elsif ($name eq 'for') { my($fortext); if ($text =~ /^(.*)\n\s*\n(.*)$/s) { ($fortext, $text) = ($1, $2); } else { ($fortext, $text) = ($text, ''); } if ($args =~ /^\s*dokuwiki\s*(.*)/) { print OUT $2, $fortext; } } elsif ($name eq 'begin') { $Begun = $args; $Begun =~ s/^\s*(\S+).*/$1/; if ($Begun eq 'dokuwiki') { print OUT $text; } return; } elsif ($name eq 'start!') { return; } elsif ($name eq 'name!') { print OUT <<"EOD;"; ====== $args Element Documentation ====== ===== NAME ===== **$args** -- $text EOD; return; } elsif ($name eq 'end!') { print OUT "\n\nGenerated by 'click-elem2man' from '$Filename' on $today.\n"; return; } print OUT ' ' x @Over, "! " if $text =~ /\S/ && @Over && $dokuwiki_dl; print OUT dokuwikiize($text), "\n"; print OUT "\n" if $name eq 'head1'; } ########### ## main ## my $do_section_func = \&nroff_do_section; my $do_text_func = \&nroffize; my $filename_func; sub do_section ($$$) { my($name, $args, $text) = @_; my(@text) = split(/^(=\w.*)$/m, $text); push @text, '' if !@text; my($i); @Over = (); undef $Begun; for ($i = 0; $i < @text; ) { &$do_section_func($name, $args, $text[$i]); ($name, $args) = ($text[$i+1] =~ /=(\w+)\s*(.*)/) if ($i < @text - 1); $i += 2; } &$do_section_func('back', '', '') while @Over; print STDERR "click-elem2man: $Filename: '=begin' not closed by end of section\n" if $Begun; } sub process_processing ($) { my($t) = @_; return undef if !defined($t); if (exists($processing_constants{$t})) { $t = $processing_constants{$t}; } $t =~ tr/\" \t//d; $t =~ s{\A([^/]*)\Z}{$1/$1}; $processing_text{$t}; } sub process_one_portcount ($$) { my($t, $type) = @_; if ($t eq '0') { "no ${type}s"; } elsif ($t eq '-') { "any number of ${type}s"; } elsif ($t eq '1') { "1 $type"; } elsif ($t eq '=') { "the same number of ${type}s"; } elsif ($t =~ /^=(\++)$/) { length($1) . " more $type"; } elsif ($t =~ /^0?-1$/) { "at most 1 $type"; } elsif ($t =~ /^0?-(.*)/) { "at most $1 ${type}s"; } elsif ($t =~ /^(\d+)-$/) { "$1 or more ${type}s"; } else { "$t ${type}s"; } } sub process_portcount ($) { my($t) = @_; $t = "$t/$t" if $t !~ /\//; return 'none' if $t eq '0/0'; my($i, $o) = split(/\//, $t); process_one_portcount($i, "input") . ", " . process_one_portcount($o, "output"); } sub process_summary_section ($$) { my($summary, $file) = @_; my($i); foreach $i (split(/,/, $summary)) { $i =~ s/^\s*//; $i =~ s/\s*$//; $i = $section_headers{$i} if exists $section_headers{$i}; next if !$i; push @{$summaries_by_section{$i}}, $file; } } sub process_drivers ($) { my($driv) = @_; my(@d, $d); foreach $d ('userlevel', 'linuxmodule', 'bsdmodule', 'ns') { push @d, $d if $driv =~ /\b$d\b/; } join(', ', @d); } sub insert_section (\@\@\@$$$$) { my($sn, $sa, $st, $i, $n, $a, $t) = @_; splice @$sn, $i, 0, $n; splice @$sa, $i, 0, $a; splice @$st, $i, 0, $t; } sub insert_section2 (\@\@\@$$$\%@) { my($sn, $sa, $st, $n, $a, $t, $fis, @x) = @_; my($pos); foreach $pos (@x) { if (exists $fis->{$pos}) { insert_section(@$sn, @$sa, @$st, $fis->{$pos} + 1, $n, $a, $t); $fis->{$n} = $fis->{$pos} + 1; return; } } } sub process_comment ($$) { my($t, $filename) = @_; my($i); $Filename = $filename; # split document into sections my(@section_text, @section_args, @section_name, $bad_section, $ref); $ref = \$bad_section; while ($t =~ m{^=(\w+)( *)(.*)([\0-\377]*?)(?=^=\w|\Z)}mg) { if ($section_break{$1}) { insert_section(@section_name, @section_args, @section_text, @section_name, $1, $3, $4); $ref = \$section_text[-1]; } else { $$ref .= '=' . $1 . $2 . $3 . $4; } } # check document for sectioning errors print STDERR "click-elem2man: $Filename: warning: comment does not start with section\n" if $bad_section; my(%num_sections, %first_in_section); foreach $i (0..$#section_name) { my($n) = $section_name[$i]; print STDERR "click-elem2man: $Filename: warning: section '=$n' requires arguments\n" if $section_takes_args{$n} == 1 && !$section_args[$i]; print STDERR "click-elem2man: $Filename: warning: section '=$n' arguments ignored\n" if $section_takes_args{$n} == 0 && $section_args[$i]; print STDERR "click-elem2man: $Filename: warning: empty section '=$n'\n" if $section_takes_text{$n} == 1 && !$section_text[$i]; print STDERR "click-elem2man: $Filename: warning: section '=$n' text ignored\n" if $section_takes_text{$n} == 0 && $section_text[$i] =~ /\S/; $num_sections{$n}++; $first_in_section{$n} = $i if $num_sections{$n} == 1; } foreach $i ('a', 'c', 'd', 'n', 'e', 'title', 'io', 'processing', 'drivers') { print STDERR "click-elem2man: $Filename: warning: multiple '=$i' sections; some may be ignored\n" if $num_sections{$i} && $num_sections{$i} > 1; } # read class names from configuration arguments section $i = $first_in_section{'c'}; if (!defined($i)) { print STDERR "click-elem2man: $Filename: section '=c' missing; cannot continue\n"; return; } my(@classes, %classes); while ($section_text[$i] =~ /^\s*(\w+)\(/mg) { push @classes, $1 if !exists $classes{$1}; $classes{$1} = 1; } if (!@classes && $section_text[$i] =~ /^\s*([\w@]+)\s*$/) { push @classes, $1; $classes{$1} = 1; } if (!@classes) { print STDERR "click-elem2man: $Filename: no class definitions\n (did you forget '()' in the =c section?)\n"; return; } my($Title) = $classes[0]; # output filenames might be specified in 'title' section my(@outfiles, @outsections, $title); if (defined($first_in_section{'title'})) { $title = $section_args[ $first_in_section{'title'} ]; if (!$title) { print STDERR "click-elem2man: $Filename: '=title' section present, but empty\n"; return; } if ($title =~ /[^-.\w@+,]/) { print STDERR "click-elem2man: $Filename: strange characters in '=title', aborting\n"; return; } foreach $i (split(/\s+/, $title)) { if ($i =~ /^(.*)\((.*)\)$/) { push @outfiles, $1; push @outsections, $2; $Title = $1; } else { push @outfiles, $i; push @outsections, $section; $Title = $i; } } } else { $title = join(', ', @classes); @outfiles = @classes; @outsections = ($section) x @classes; } # open new output file if necessary my($main_outname); if ($filename_func) { $main_outname = &$filename_func($outfiles[0], $outsections[0]); if ($uninstall) { unlink($main_outname); open(OUT, ">/dev/null"); } elsif (!open(OUT, ">$main_outname")) { print STDERR "$main_outname: $!\n"; return; } } push @all_outfiles, $outfiles[0]; $all_outsections{$outfiles[0]} = $outsections[0]; $PrimaryElement = $outfiles[0]; # prepare related %RelatedSource = (); $i = $first_in_section{'a'}; if (defined($i)) { $section_text[$i] = $section_args[$i] . $section_text[$i] if $section_args[$i]; if ($section_text[$i] =~ /\A\s*(.*?)(\n\s*\n.*\Z|\Z)/s) { my($bit, $last) = ($1, $2); while ($bit =~ m{(\b[A-Z][-\w@.+=]+)([,\s]|\Z)}g) { $RelatedSource{$1} = 'n'; } $bit =~ s{([-\w@.+=]+)([,\s]|\Z)}{$1(n)$2}g; while ($bit =~ m{([-\w@.+=]+\(([0-9ln])\))}g) { $RelatedSource{$1} = $2; } $section_text[$i] = $bit . $last; } } map(delete $RelatedSource{$_}, @outfiles); @Related = sort { length($b) <=> length($a) } (keys %RelatedSource, @classes); # front matter my($oneliner) = (@classes == 1 ? "Click element" : "Click elements"); $i = $first_in_section{'s'}; my($summary_section) = ''; if (defined($i)) { $summary_section = $section_args[$i]; process_summary_section($summary_section, $outfiles[0]); $section_text[$i] =~ s/\n\s*\n/\n/g; my($t) = &$do_text_func($section_text[$i]); $oneliner .= ";\n" . $t; $oneliner =~ s/\n(^\.)/ /g; $all_roff_summaries{$outfiles[0]} = $t . "\n"; } else { # avoid uninitialized value warns $all_roff_summaries{$outfiles[0]} = ''; } # deprecation if (defined($first_in_section{'deprecated'})) { $deprecated{$outfiles[0]} = $section_text[$first_in_section{'deprecated'}]; } # package if (!defined($first_in_section{'package'}) && defined($package)) { PACKAGE: { my($pkg) = $package; if ($pkg eq 'DEFAULT') { $pkg = $1 if $Filename =~ m|\belements/(\w+)/|; $pkg = 'standard' if $Filename =~ m|\binclude/click/standard/|; last PACKAGE if $pkg eq 'DEFAULT'; $pkg = exists($default_packages{$pkg}) ? $default_packages{$pkg} : $pkg; } insert_section2(@section_name, @section_args, @section_text, 'package', '', $pkg, %first_in_section, 'drivers', 'processing', 'io', 'c'); }} # drivers my($drivers); if ($docrequires{$Title} && ($drivers = process_drivers($docrequires{$Title})) && !defined($first_in_section{'drivers'})) { insert_section2(@section_name, @section_args, @section_text, 'drivers', '', $drivers, %first_in_section, 'processing', 'io', 'c'); } # processing if (!defined($first_in_section{'processing'})) { PROCESSING: { # can we figure out the processing type? last PROCESSING if (defined($first_in_section{'io'}) && $section_text[$first_in_section{'io'}] =~ /None/i) || (defined($portcount{$Title}) && ($portcount{$Title} eq '0' || $portcount{$Title} eq '0/0')); my($ptype) = process_processing($processing{$Title}); insert_section2(@section_name, @section_args, @section_text, 'processing', '', $ptype, %first_in_section, 'io', 'c') if $ptype; }} # input/output if (!defined($first_in_section{'io'})) { PORTCOUNT: { last PORTCOUNT if !defined($portcount{$Title}); insert_section2(@section_name, @section_args, @section_text, 'io', '', process_portcount($portcount{$Title}), %first_in_section, 'c'); }} # initial sections insert_section(@section_name, @section_args, @section_text, 0, 'start!', $outsections[0], $title); insert_section(@section_name, @section_args, @section_text, 1, 'name!', $title, $oneliner); insert_section(@section_name, @section_args, @section_text, @section_name, 'end!', $outsections[0], $title); # output my($special_section) = 0; for ($i = 0; $i < @section_text; $i++) { my($s) = $section_name[$i]; my($x) = $section_text[$i]; my($was_special_section) = $special_section; $special_section = 0; if ($s eq 'c') { $x =~ s{(\S\s*)\n}{$1N<>\n}g; $x =~ s{N<>\n*\z}{}; do_section('head1', 'SYNOPSIS', $x); } elsif ($s eq 'package' || $s eq 'io' || $s eq 'processing' || $s eq 'drivers') { $x =~ s/\A\s+//; $x =~ s/\s+\Z//; do_section('text', '', 'B<' . $infosection_name{$s} . '>: ' . $x . 'N<>'); } elsif ($s eq 'io') { do_section('head1', 'INPUTS AND OUTPUTS', $x); } elsif ($s eq 'processing') { do_section('head1', 'PROCESSING TYPE', $x); } elsif ($s eq 'd') { do_section('head1', 'DESCRIPTION', $x); } elsif ($s eq 'n') { do_section('head1', 'NOTES', $x); } elsif ($s eq 'e') { do_section('head1', 'EXAMPLES', $x); } elsif ($s eq 'h') { my($t) = "=over 5\n"; while ($i < @section_text && $section_name[$i] eq 'h') { if ($section_args[$i] =~ /\A\s*(.*?)\s*"(.*?)"\s*\Z/ || $section_args[$i] =~ /\A\s*(.*?)\s*(\S+)\s*\Z/) { $t .= "=item B<$1> ($2)\n"; } else { print STDERR "click-elem2man: $Filename: bad handler section arguments ('=h $section_args[$i]')\n"; $t .= "=item B<$section_args[$i]>\n"; } $t .= $section_text[$i] . "\n"; $i++; } $i--; do_section('head1', 'ELEMENT HANDLERS', $t); } elsif ($s eq 'a') { do_section('head1', 'SEE ALSO', $x); } elsif ($s eq 'title' || $s eq 's' || $s eq 'deprecated') { # nada } else { do_section($s, $section_args[$i], $x); } } # close output file & make links if appropriate if ($filename_func) { close OUT; for ($i = 1; $i < @outfiles; $i++) { my($outname) = &$filename_func($outfiles[$i], $outsections[$i]); unlink($outname); if ($uninstall) { # do nothing } elsif (link $main_outname, $outname) { push @all_outfiles, $outfiles[$i]; $all_outsections{$outfiles[$i]} = $outsections[$i]; process_summary_section($summary_section, $outfiles[$i]); $all_roff_summaries{$outfiles[$i]} = $all_roff_summaries{$outfiles[0]}; } else { print STDERR "click-elem2man: $outname: $!\n"; } } } } sub process_file ($;$) { my($filename, $text) = @_; $filename = "include/$1" if !-e $filename && $filename =~ /^<(.*)>$/s; $filename = "$PREFIX/$filename" if !-e $filename && defined($PREFIX); return if !-e $filename; if (!defined($text)) { if (!open(IN, $filename)) { print STDERR "click-elem2man: $filename: $!\n"; return; } $text = ; close IN; } print "$filename??\n" if !defined $text; foreach $_ (split(m{(/\*.*?\*/)}s, $text)) { if (/^\/\*/ && /^[\/*\s]+=/) { s/^(.*?)\t/$1 . (' ' x (8 - (length($1) % 8)))/egm while /\t/; s/^\/\*\s*//g; s/\s*\*\/$//g; s/^ ?\* ?//gm; process_comment($_, $filename); } } } sub xmlunquote ($) { my($x) = @_; if ($x =~ /&/) { $x =~ s/<//g; $x =~ s/&/&/g; $x =~ s/&#([0-9]+);/chr($1)/eg; $x =~ s/&#x([0-9a-zA-Z]+);/chr(oct("0x$1"))/eg; } $x; } sub read_elementmap_text ($;$) { my($text, $headerhash) = @_; $headerhash = {} if !defined($headerhash); local($_); foreach $_ (split(/\n+/, $text)) { next if !/^{xmlunquote($hf)} = 1 if defined($hf); } } } sub read_elementmap ($;$) { my($fn, $headerhash) = @_; open(E, $fn) || die "$fn: $!\n"; local($/) = undef; read_elementmap_text(, $headerhash); close E; } sub process_file_set ($$) { my($fn, $fnhash) = @_; if (open(IN, ($fn eq '-' ? "<&STDIN" : $fn))) { my(@a, @b, $t, $x); $t = ; close IN; # Parse file; click-buildtool gets special treatment if ($t =~ /\A#.*click-buildtool findelem/) { $t =~ s/^#.*//mg; foreach $_ (split(/\n+/, $t)) { if (/^\S+\s+<(\S+?)>/) { $fnhash->{"include/$1"} = 1; } elsif (/^\S+\s+"(\S+?)"/) { $fnhash->{$1} = 1; } else { s/.cc$/.hh/; $fnhash->{$_} = 1; } } } elsif ($t =~ /\A<\?xml/) { read_elementmap_text($t, $fnhash); } elsif ($t =~ /[\(\)]/) { die if $fn eq '-'; $fnhash->{$fn} = 1; } else { $t =~ s/^#.*//mg; foreach $_ (split(/\s+/, $t)) { if ($t =~ /[*?\[]/) { foreach my $x (glob($t)) { $fnhash->{$x} = 1; } } else { $fnhash->{$_} = 1; } } } } else { print STDERR "click-elem2man: $fn: $!\n"; } } sub uniq (@) { my(@a, $x); foreach $x (@_) { push @a, $x if !@a || $a[-1] ne $x; } @a; } sub driver_requirement ($) { my($req) = @_; my($d) = 0; $d |= 1 if $req =~ /\buserlevel\b/; $d |= 2 if $req =~ /\blinuxmodule\b/; $d |= 4 if $req =~ /\bbsdmodule\b/; $d |= 8 if $req =~ /\bns\b/; $d ? $d : 15; } my(%requires_fixed); sub fix_one_requires ($) { my($req) = @_; my($driver) = driver_requirement($req); my($t, $i); my($changes) = ''; foreach $t (split(/\s+/, $req)) { if (defined($requires{$t})) { my($treq) = $requires{$t}; if (!$requires_fixed{$t}) { $requires_fixed{$t} = 1; for ($i = 0; $i < @$treq; $i++) { $treq->[$i] = &fix_one_requires($treq->[$i]); } } for ($i = 0; $i < @$treq; $i++) { if ($driver & driver_requirement($treq->[$i])) { $changes .= " " . $treq->[$i]; } } } } if ($changes) { join(' ', uniq(sort {$a cmp $b} split(/\s+/, $req . $changes))); } else { $req; } } sub fix_requires () { my($k); foreach $k (keys %docrequires) { $docrequires{$k} = fix_one_requires($docrequires{$k}); } } # main program: parse options sub usage () { print STDERR "Usage: click-elem2man [OPTIONS] FILES... Try 'click-elem2man --help' for more information.\n"; exit 1; } sub help () { print STDERR <<"EOD;"; 'Click-elem2man' translates Click element documentation into manual pages. Usage: click-elem2man [-l | -L] [-d DIRECTORY] FILE... Each FILE is a Click header file, a list of Click header files, or the output of click-mkelemmap. '-' means standard input. Options: -f, --files FILE Read header filenames from FILE. -e, --elementmap EMAP Read information about other elements from EMAP. -d, --directory DIR Place generated manual pages in directory DIR. -P, --package PKG Elements are in PKG package, or say 'DEFAULT'. -l, --list Generate the elements(n) manual page as well. -L, --extend-list Extend an existing elements(n) manual page. -p, --prefix PFX Look for header files in PFX after looking in '.'. --dokuwiki Generate dokuwiki source instead of manual pages. --dokuwiki-dl Generate dokuwiki source using dl plugin. -u, --uninstall Remove existing manual pages. -h, --help Print this message and exit. Report bugs to . EOD; exit 0; } undef $/; my(@files, $fn, $elementlist, $any_files, $directory, $output_type); $output_type = 'nroff'; while (@ARGV) { $_ = shift @ARGV; if (/^-d$/ || /^--directory$/) { usage() if !@ARGV; $directory = shift @ARGV; } elsif (/^--directory=(.*)$/) { $directory = $1; } elsif (/^-P$/ || /^--package$/) { usage() if !@ARGV; $package = shift @ARGV; } elsif (/^--package=(.*)$/) { $package = $1; } elsif (/^-f$/ || /^--files$/) { usage() if !@ARGV; push @files, shift @ARGV; $any_files = 1; } elsif (/^--files=(.*)$/) { push @files, $1; $any_files = 1; } elsif (/^-l$/ || /^--list$/) { $elementlist = 1; } elsif (/^-L$/ || /^--extend-list$/) { $elementlist = 2; } elsif (/^-e$/ || /^--elementmap$/) { usage() if !@ARGV; read_elementmap(shift @ARGV); } elsif (/^--elementmap=(.*)$/) { read_elementmap($1); } elsif (/^-p$/ || /^--prefix$/) { usage() if !@ARGV; $PREFIX = shift @ARGV; } elsif (/^--prefix=(.*)$/) { $PREFIX = $1; } elsif (/^-u$/ || /^--uninstall$/) { $uninstall = 1; } elsif (/^-h$/ || /^--help$/) { help(); } elsif (/^--dokuwiki$/ || /^--doku$/) { $do_section_func = \&dokuwiki_do_section; $do_text_func = \&dokuwikiize; $output_type = 'dokuwiki'; } elsif (/^--dokuwiki-dl$/) { $do_section_func = \&dokuwiki_do_section; $do_text_func = \&dokuwikiize; $output_type = 'dokuwiki'; $dokuwiki_dl = 1; } elsif (/^-./) { usage(); } elsif (/^-$/) { push @files, "-"; $any_files = 1; } else { push @files, glob($_); $any_files = 1; } } push @files, "-" if !$any_files; sub nroff_filename_func ($$) { my($name, $sec) = @_; "$directory/$name.$sec"; } sub dokuwiki_filename_func ($$) { my($name, $sec) = @_; if ($sec eq 'n') { "$directory/" . lc($name) . ".txt"; } else { "$directory/" . lc($name) . "-$sec.txt"; } } if ($directory) { $filename_func = ($output_type eq 'dokuwiki' ? \&dokuwiki_filename_func : \&nroff_filename_func); } umask(022); if ($uninstall) { open(OUT, ">/dev/null"); } elsif (!$directory) { open(OUT, ">&STDOUT"); } # read file sets my(%fnhash); foreach $fn (@files) { process_file_set($fn, \%fnhash); } fix_requires(); # process files foreach $fn (keys(%fnhash)) { process_file($fn); } if ($uninstall || !$directory) { close OUT; } # printing element list # sub read_elementlist ($) { my($fn) = @_; local($/) = "\n"; if (!open(IN, $fn)) { print STDERR "click-elem2man: $fn: $!\n"; return; } my($section, $active, $cur_name, $cur_section, %new_summary); $active = 0; while () { if (/^\.SS \"(.*)\"/) { $section = $1; } elsif (/^\.TP 20/) { $active = 1; } elsif ($active == 1 && /^\.M (\S+) (\S+)/) { if (!exists $all_outsections{$1}) { $active = 2; $cur_name = $1; $new_summary{$1} = 1; $all_outsections{$1} = $2; $all_roff_summaries{$1} = ''; push @{$summaries_by_section{$section}}, $1; push @all_outfiles, $1; } elsif ($new_summary{$1}) { push @{$summaries_by_section{$section}}, $1; $active = 3; } else { $active = 3; } } elsif (/^\.PD/) { $active = 0; } elsif (/^\.SH \"ALPHABETICAL/) { last; } elsif ($active == 2) { $all_roff_summaries{$cur_name} .= $_; } } } my(%el_generated); sub one_elementlist (@) { my($t); $t .= ".PP\n.PD 0\n"; foreach $_ (sort { lc($a) cmp lc($b) } @_) { $t .= ".TP 20\n.M " . $_ . " " . $all_outsections{$_}; $t .= " \" (deprecated)\"" if $deprecated{$_}; $t .= "\n"; $t .= $all_roff_summaries{$_} if $all_roff_summaries{$_}; $el_generated{$_} = 1; } $t . ".PD\n"; } my(@Links, $Text); sub one_summary ($@) { my($name, @elts) = @_; print STDERR "click-elem2man: warning: category '", $name, "' begins with a lowercase letter (used in @elts)\n" if $name =~ /^[a-z]/; my($a) = $name; $a =~ s{([+\&\#\"\000-\037\177-\377])}{sprintf("%%%02X", $1)}eg; $a =~ tr/ /+/; my($x) = $name; $x =~ s{ }{ }g; if (@elts) { push @Links, "$x"; $Text .= ".SS \"$name\"\n"; $Text .= one_elementlist(@elts); } } sub write_elementlist ($$) { my($enamebase, $l_uninstall) = @_; if ($filename_func) { my($fn) = &$filename_func($enamebase, $section); if ($l_uninstall) { unlink($fn); return; } if (!open(OUT, ">$fn")) { print STDERR "click-elem2man: $fn: $!\n"; return; } } elsif ($uninstall) { return; } print OUT <<"EOD;"; .\\" -*- mode: nroff -*- .\\" Generated by 'click-elem2man' $nroff_prologue .TH "\U$enamebase\E" $section "$today" "Click" .SH "NAME" $enamebase \- documented Click element classes .SH "DESCRIPTION" This page lists all Click element classes that have manual page documentation. EOD; $Text = ""; my(%did_section, $sec); # erase 'Miscellaneous' section, if any delete $summaries_by_section{'Miscellaneous'}; my($i) = 0; foreach $sec ((map { $i++ % 2 ? ($_) : () } @section_headers), (sort { lc($a) cmp lc($b) } keys(%summaries_by_section))) { next if $did_section{$sec}; one_summary($sec, @{$summaries_by_section{$sec}}); $did_section{$sec} = 1; } one_summary('Miscellaneous', grep { !$el_generated{$_} } @all_outfiles); my($links) = join(" - ", @Links); print OUT <<"EOD;"; .\\"html

By Function: .\\"html $links
.\\"html Alphabetical List

EOD; print OUT ".SH \"BY FUNCTION\"\n"; print OUT $Text; print OUT ".SH \"ALPHABETICAL LIST\"\n"; print OUT one_elementlist(@all_outfiles); close OUT if $filename_func; } sub read_main_elementlist () { if ($filename_func) { my($fn) = &$filename_func('elements', $section); $fn = &$filename_func('elements-click', $section) if !-r $fn; read_elementlist($fn) if -r $fn && $elementlist > 1; } } if ($elementlist && @all_outfiles) { # erase record of elements added on uninstall if ($elementlist > 1 && $uninstall) { %summaries_by_section = (); @all_outfiles = (); } # package-specific elementlist if (defined($package)) { my($fename) = ($package eq "DEFAULT" ? "elements-click" : "elements-$package"); write_elementlist($fename, $uninstall); } read_main_elementlist(); write_elementlist("elements", 0); }