Id attributes in <a name> is no longer being generated.
[grutatxt.git] / Grutatxt.pm
blob115fc07f056712106bc7a89ef5d7967540072be3
1 #####################################################################
3 # Grutatxt - A text to HTML (and other things) converter
5 # Copyright (C) 2000/2004 Angel Ortega <angel@triptico.com>
7 # This program is free software; you can redistribute it and/or
8 # modify it under the terms of the GNU General Public License
9 # as published by the Free Software Foundation; either version 2
10 # of the License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with this program; if not, write to the Free Software
19 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
21 # http://www.triptico.com
23 #####################################################################
25 package Grutatxt;
27 use locale;
29 $VERSION = '2.0.10';
31 =pod
33 =head1 NAME
35 Grutatxt - Text to HTML (and other formats) converter
37 =head1 SYNOPSIS
39 use Grutatxt;
41 # create a new Grutatxt converter object
42 $grutatxt=new Grutatxt();
44 # process a Grutatxt format string
45 @output=$grutatxt->process($text);
47 # idem for a file
48 @output2=$grutatxt->process_file($file);
50 =head1 DESCRIPTION
52 Grutatxt is a module to process text documents in
53 a special markup format (also called Grutatxt), very
54 similar to plain ASCII text. These documents can be
55 converted to HTML, troff or man.
57 The markup is designed to be fairly intuitive and
58 straightforward and can include headings, bold and italic
59 text effects, bulleted, numbered and definition lists, URLs,
60 function and variable names, preformatted text, horizontal
61 separators and tables. Special marks can be inserted in the
62 text and a heading-based structural index can be obtained
63 from it.
65 A comprehensive description of the markup is defined in
66 the README file, included with the Grutatxt package (it is
67 written in Grutatxt format itself, so it can be converted
68 using the I<grutatxt> tool to any of the supported formats).
69 The latest version (and more information) can be retrieved
70 from the Grutatxt home page at:
72 http://www.triptico.com/software/grutatxt.html
74 =head1 FUNCTIONS AND METHODS
76 =head2 B<new>
78 $grutatxt=new Grutatxt([ "mode" => $mode, ]
79 [ "title" => \$title, ]
80 [ "marks" => \@marks, ]
81 [ "index" => \@index, ]
82 [ "abstract" => \$abstract, ]
83 [ "strip-parens" => $bool, ]
84 [ "strip-dollars" => $bool, ]
85 [ %driver_specific_arguments ] );
87 Creates a new Grutatxt object instance. All parameters are
88 optional.
90 =over 4
92 =item I<mode>
94 Output format. Can be HTML, troff or man. HTML is used if not specified.
96 =item I<title>
98 If I<title> is specified as a reference to scalar, the first
99 level 1 heading found in the text is stored inside it.
101 =item I<marks>
103 Marks in the Grutatxt markup are created by inserting the
104 string <-> alone in a line. If I<marks> is specified as a
105 reference to array, it will be filled with the subscripts
106 (relative to the output array) of the lines where the marks
107 are found in the text.
109 =item I<index>
111 If I<index> is specified as a reference to array, it will
112 be filled with strings in the format
114 level,heading
116 This information can be used to build a table of contents
117 of the processed text.
119 =item I<strip-parens>
121 Function names in the Grutatxt markup are strings of
122 alphanumeric characters immediately followed by a pair
123 of open and close parentheses. If this boolean value is
124 set, function names found in the processed text will have
125 their parentheses deleted.
127 =item I<strip-dollars>
129 Variable names in the Grutatxt markup are strings of
130 alphanumeric characters preceded by a dollar sign.
131 If this boolean value is set, variable names found in
132 the processed text will have the dollar sign deleted.
134 =item I<abstract>
136 The I<abstract> of a Grutatxt document is the fragment of text
137 from the beginning of the document to the end of the first
138 paragraph after the title. If I<abstract> is specified as a
139 reference to scalar, it will contain (after each call to the
140 B<process()> method) the subscript of the element of the output
141 array that marks the end of the subject.
143 =back
145 =cut
147 sub new
149 my ($class,%args) = @_;
150 my ($gh);
152 $args{'mode'} ||= 'HTML';
154 $class .= "::" . $args{'mode'};
156 $gh = new $class(%args);
158 return($gh);
162 sub escape
163 # escapes special characters, ignoring passthrough code
165 my ($gh, $l) = @_;
167 # splits between << and >>
168 my (@l) = split(/(<<|>>)/, $l);
170 @l = map {
171 my $l = $_;
173 # escape only text outside << and >>
174 unless($l eq "<<" .. $l eq ">>")
176 $l = $gh->_escape($l);
179 $_ = $l;
180 } @l;
182 # join again, stripping << and >>
183 $l = join('', grep(!/^(<<|>>)$/, @l));
185 return($l);
189 =head2 B<process>
191 @output=$grutatxt->process($text);
193 Processes a text in Grutatxt format. The result is returned
194 as an array of lines.
196 =cut
198 sub process
200 my ($gh,$content) = @_;
201 my ($p);
203 # clean output
204 @{$gh->{'o'}} = ();
206 # clean title and paragraph numbers
207 $gh->{'-title'} = "";
208 $gh->{'-p'} = 0;
210 # clean marks
211 @{$gh->{'marks'}} = () if ref($gh->{'marks'});
213 # clean index
214 @{$gh->{'index'}} = () if ref($gh->{'index'});
216 # reset abstract line
217 ${$gh->{'abstract'}} = 0 if ref($gh->{'abstract'});
219 # insert prefix
220 $gh->_prefix();
222 $gh->{'-mode'} = undef;
224 foreach my $l (split(/\n/,$content))
226 # inline data (passthrough)
227 if($l =~ /^<<$/ .. $l =~ /^>>$/)
229 $gh->_inline($l);
230 next;
233 # marks
234 if($l =~ /^\s*<\->\s*$/)
236 push(@{$gh->{'marks'}},scalar(@{$gh->{'o'}}))
237 if ref($gh->{'marks'});
239 next;
242 # escape possibly dangerous characters
243 $l = $gh->escape($l);
245 # empty lines
246 $l =~ s/^\r$//ge;
247 if($l =~ s/^$/$gh->_empty_line()/ge)
249 # mark the abstract end
250 if($gh->{'-title'})
252 $gh->{'-p'}++;
254 # mark abstract if it's the
255 # second paragraph from the title
256 ${$gh->{'abstract'}} = scalar(@{$gh->{'o'}})-1
257 if $gh->{'-p'} == 2;
261 if($gh->{'-process-urls'})
263 # URLs followed by a parenthesized phrase
264 $l =~ s/(https?:\/\/\S+)\s+\(([^\)]+)\)/$gh->_url($1,$2)/ge;
265 $l =~ s/(ftps?:\/\/\S+)\s+\(([^\)]+)\)/$gh->_url($1,$2)/ge;
266 $l =~ s/(file:\/?\S+)\s+\(([^\)]+)\)/$gh->_url($1,$2)/ge;
267 $l =~ s|(\s+)\./(\S+)\s+\(([^\)]+)\)|$1.$gh->_url($2,$3)|ge;
268 $l =~ s|^\./(\S+)\s+\(([^\)]+)\)|$gh->_url($1,$2)|ge;
270 # URLs without phrase
271 $l =~ s/([^=][^\"])(https?:\/\/\S+)/$1.$gh->_url($2)/ge;
272 $l =~ s/([^=][^\"])(ftps?:\/\/\S+)/$1.$gh->_url($2)/ge;
273 $l =~ s/([^=][^\"])(file:\/?\S+)/$1.$gh->_url($2)/ge;
274 $l =~ s|(\s+)\./(\S+)|$1.$gh->_url($2)|ge;
276 $l =~ s/^(https?:\/\/\S+)/$gh->_url($1)/ge;
277 $l =~ s/^(ftps?:\/\/\S+)/$gh->_url($1)/ge;
278 $l =~ s/^(file:\/?\S+)/$gh->_url($1)/ge;
279 $l =~ s|^\./(\S+)|$gh->_url($1)|ge;
282 # change '''text''' and *text* into strong emphasis
283 $l =~ s/\'\'\'([^\'][^\'][^\']*)\'\'\'/$gh->_strong($1)/ge;
284 $l =~ s/\*(\S[^\*]+\S)\*/$gh->_strong($1)/ge;
285 $l =~ s/\*(\S+)\*/$gh->_strong($1)/ge;
287 # change ''text'' and _text_ into emphasis
288 $l =~ s/\'\'([^\'][^\']*)\'\'/$gh->_em($1)/ge;
289 $l =~ s/\b_(\S[^_]*\S)_\b/$gh->_em($1)/ge;
290 $l =~ s/\b_(\S+)_\b/$gh->_em($1)/ge;
292 # enclose function names
293 if($gh->{'strip-parens'})
295 $l =~ s/(\w+)\(\)/$gh->_funcname($1)/ge;
297 else
299 $l =~ s/(\w+)\(\)/$gh->_funcname($1."()")/ge;
302 # enclose variable names
303 if($gh->{'strip-dollars'})
305 $l =~ s/\$([\w_\.]+)/$gh->_varname($1)/ge;
307 else
309 $l =~ s/(\$[\w_\.]+)/$gh->_varname($1)/ge;
313 # main switch
316 # definition list
317 if($l =~ s/^\s\*\s+([\w\s\-\(\)]+)\:\s+/$gh->_dl($1)/e)
319 $gh->{'-mode-elems'} ++;
322 # unsorted list
323 elsif($gh->{'-mode'} ne "pre" and
324 ($l =~ s/^(\s+)\*\s+/$gh->_unsorted_list($1)/e or
325 $l =~ s/^(\s+)\-\s+/$gh->_unsorted_list($1)/e))
327 $gh->{'-mode-elems'} ++;
330 # sorted list
331 elsif($gh->{'-mode'} ne "pre" and
332 ($l =~ s/^(\s+)\#\s+/$gh->_ordered_list($1)/e or
333 $l =~ s/^(\s+)1\s+/$gh->_ordered_list($1)/e))
335 $gh->{'-mode-elems'} ++;
338 # quoted block
339 elsif($l =~ s/^\s\"/$gh->_blockquote()/e)
343 # table rows
344 elsif($l =~ s/^\s*\|(.*)\|\s*$/$gh->_table_row($1)/e)
346 $gh->{'-mode-elems'} ++;
349 # table heading / end of row
350 elsif($l =~ s/^\s*(\+[-\+\|]+\+)\s*$/$gh->_table($1)/e)
354 # preformatted text
355 elsif($l =~ s/^(\s.*)$/$gh->_pre($1)/e)
359 # anything else
360 else
362 # back to normal mode
363 $gh->_new_mode(undef);
366 # 1 level heading
367 $l =~ s/^(=+)\s*$/$gh->_process_heading(1,$1)/e;
369 # 2 level heading
370 $l =~ s/^(-+)\s*$/$gh->_process_heading(2,$1)/e;
372 # 3 level heading
373 $l =~ s/^(~+)\s*$/$gh->_process_heading(3,$1)/e;
375 # change ------ into hr
376 $l =~ s/^----*$/$gh->_hr()/e;
378 # push finally
379 $gh->_push($l) if $l;
382 # flush
383 $gh->_new_mode(undef);
385 # postfix
386 $gh->_postfix();
388 # set title
389 ${$gh->{'title'}} = $gh->{'-title'} if ref($gh->{'title'});
391 # set abstract, if not set
392 ${$gh->{'abstract'}} = scalar(@{$gh->{'o'}})
393 if ref($gh->{'abstract'}) and not ${$gh->{'abstract'}};
395 return(@{$gh->{'o'}});
399 =head2 B<process_file>
401 @output=$grutatxt->process_file($filename);
403 Processes a file in Grutatxt format.
405 =cut
407 sub process_file
409 my ($gh,$file) = @_;
411 open F, $file or return(undef);
413 my ($content) = join('',<F>);
414 close F;
416 return($gh->process($content));
420 sub _push
422 my ($gh,$l) = @_;
424 push(@{$gh->{'o'}},$l);
428 sub _process_heading
430 my ($gh,$level,$hd) = @_;
431 my ($l);
433 $l = pop(@{$gh->{'o'}});
435 if($l eq $gh->_empty_line())
437 $gh->_push($l);
438 return($hd);
441 # store title
442 $gh->{'-title'} = $l if $level == 1 and not $gh->{'-title'};
444 # store index
445 if(ref($gh->{'index'}))
447 push(@{$gh->{'index'}},"$level,$l");
450 return($gh->_heading($level,$l));
454 sub _calc_col_span
456 my ($gh,$l) = @_;
457 my (@spans);
459 # strip first + and all -
460 $l =~ s/^\+//;
461 $l =~ s/-//g;
463 my ($t) = 1; @spans = ();
464 for(my $n = 0;$n < length($l);$n++)
466 if(substr($l,$n,1) eq '+')
468 push(@spans,$t);
469 $t = 1;
471 else
473 # it's a colspan mark:
474 # increment
475 $t++;
479 return(@spans);
483 sub _table_row
485 my ($gh,$str) = @_;
487 my @s = split(/\|/,$str);
489 for(my $n = 0;$n < scalar(@s);$n++)
491 ${$gh->{'-table'}}[$n] .= ' ' . $s[$n];
494 push(@{$gh->{'-table-raw'}}, $str);
496 return("");
500 sub _pre
502 my ($gh,$l) = @_;
504 # if any other mode is active, add to it
505 if($gh->{'-mode'} and $gh->{'-mode'} ne "pre")
507 $l =~ s/^\s+//;
509 my ($a) = pop(@{$gh->{'o'}})." ".$l;
510 $gh->_push($a);
511 $l = "";
513 else
515 $gh->_new_mode("pre");
518 return($l);
522 sub _multilevel_list
524 my ($gh, $str, $ind) = @_;
525 my (@l,$level);
527 @l = @{$gh->{$str}};
528 $ind = length($ind);
529 $level = 0;
531 if($l[-1] < $ind)
533 # if last level is less indented, increase
534 # nesting level
535 push(@l, $ind);
536 $level++;
538 elsif($l[-1] > $ind)
540 # if last level is more indented, decrease
541 # levels until the same is found (or back to
542 # the beginning if not)
543 while(pop(@l))
545 $level--;
546 last if $l[-1] == $ind;
550 $gh->{$str} = \@l;
552 return($level);
556 sub _unsorted_list
558 my ($gh, $ind) = @_;
560 return($gh->_ul($gh->_multilevel_list('-ul-levels', $ind)));
564 sub _ordered_list
566 my ($gh, $ind) = @_;
568 return($gh->_ol($gh->_multilevel_list('-ol-levels', $ind)));
572 # empty stubs for falling through the superclass
574 sub _inline { my ($gh,$l) = @_; $l; }
575 sub _escape { my ($gh,$l) = @_; $l; }
576 sub _empty_line { my ($gh) = @_; ""; }
577 sub _url { my ($gh,$url,$label) = @_; ""; }
578 sub _strong { my ($gh,$str) = @_; $str; }
579 sub _em { my ($gh,$str) = @_; $str; }
580 sub _funcname { my ($gh,$str) = @_; $str; }
581 sub _varname { my ($gh,$str) = @_; $str; }
582 sub _new_mode { my ($gh,$mode) = @_; }
583 sub _dl { my ($gh,$str) = @_; $str; }
584 sub _ul { my ($gh,$level) = @_; ""; }
585 sub _ol { my ($gh,$level) = @_; ""; }
586 sub _blockquote { my ($gh,$str) = @_; $str; }
587 sub _hr { my ($gh) = @_; "" }
588 sub _heading { my ($gh,$level,$l) = @_; $l; }
589 sub _table { my ($gh,$str) = @_; $str; }
590 sub _prefix { my ($gh) = @_; }
591 sub _postfix { my ($gh) = @_; }
593 ###########################################################
595 =head1 DRIVER SPECIFIC INFORMATION
597 =cut
599 ###########################################################
600 # HTML Driver
602 package Grutatxt::HTML;
604 @ISA = ("Grutatxt");
606 =head2 HTML Driver
608 The additional parameters for a new Grutatxt object are:
610 =over 4
612 =item I<table-headers>
614 If this boolean value is set, the first row in tables
615 is assumed to be the heading and rendered using <th>
616 instead of <td> tags.
618 =item I<center-tables>
620 If this boolean value is set, tables are centered.
622 =item I<expand-tables>
624 If this boolean value is set, tables are expanded (width 100%).
626 =item I<dl-as-dl>
628 If this boolean value is set, definition lists will be
629 rendered using <dl>, <dt> and <dd> instead of tables.
631 =item I<header-offset>
633 Offset to be summed to the heading level when rendering
634 <h?> tags (default is 0).
636 =item I<class-oddeven>
638 If this boolean value is set, tables will be rendered
639 with an "oddeven" CSS class, and rows alternately classed
640 as "even" or "odd". If it's not set, no CSS class info
641 is added to tables.
643 =back
645 =cut
647 sub new
649 my ($class,%args) = @_;
650 my ($gh);
652 bless(\%args,$class);
653 $gh = \%args;
655 $gh->{'-process-urls'} = 1;
657 return($gh);
661 sub _inline
663 my ($gh,$l) = @_;
665 # accept unnamed and HTML inlines
666 if($l =~ /^<<$/ or $l =~ /^<<\s*html$/i)
668 $gh->{'-inline'} = "HTML";
669 return;
672 if($l =~ /^>>$/)
674 delete $gh->{'-inline'};
675 return;
678 if($gh->{'-inline'} eq "HTML")
680 $gh->_push($l);
685 sub _escape
687 my ($gh, $l) = @_;
689 $l =~ s/&/&amp;/g;
690 $l =~ s/</&lt;/g;
691 $l =~ s/>/&gt;/g;
693 return($l);
697 sub _empty_line
699 my ($gh) = @_;
701 return("<p>");
705 sub _url
707 my ($gh,$url,$label) = @_;
709 $label = $url unless $label;
711 return("<a href=\"$url\">$label</a>");
715 sub _strong
717 my ($gh,$str) = @_;
718 return("<strong class='strong'>$str</strong>");
722 sub _em
724 my ($gh,$str) = @_;
725 return("<em class='em'>$str</em>");
729 sub _funcname
731 my ($gh,$str) = @_;
732 return("<code class='funcname'>$str</code>");
736 sub _varname
738 my ($gh,$str) = @_;
739 return("<code class='var'>$str</code>");
743 sub _new_mode
745 my ($gh,$mode,$params) = @_;
747 if($mode ne $gh->{'-mode'})
749 my $tag;
751 # flush previous mode
752 $gh->_push($gh->{'-mode-elem-close'})
753 if $gh->{'-mode-elem-close'} and $gh->{'-mode-elems'};
755 # clean list levels
756 if($gh->{'-mode'} eq "ul")
758 $gh->_push("</ul>" x scalar(@{$gh->{'-ul-levels'}}));
760 elsif($gh->{'-mode'} eq "ol")
762 $gh->_push("</ol>" x scalar(@{$gh->{'-ol-levels'}}));
764 elsif($gh->{'-mode'})
766 $gh->_push("</$gh->{'-mode'}>");
769 # send new one
770 $tag = $params ? "<$mode $params>" : "<$mode>";
771 $gh->_push($tag) if $mode;
773 $gh->{'-mode'} = $mode;
774 $gh->{'-mode-elems'} = 0;
776 # clean previous lists
777 $gh->{'-ul-levels'} = undef;
778 $gh->{'-ol-levels'} = undef;
783 sub _dl
785 my ($gh,$str) = @_;
786 my ($ret) = '';
788 if($gh->{'dl-as-dl'})
790 $gh->_new_mode("dl");
791 $ret .= "<dt><strong class='term'>$str</strong><dd>";
793 else
795 $gh->_new_mode("table");
796 $ret .= "<tr><td valign='top'><strong class='term'>$1</strong>&nbsp;&nbsp;</td><td valign='top'>";
799 return($ret);
803 sub _ul
805 my ($gh, $levels) = @_;
806 my ($ret);
808 $ret = '';
810 if($levels > 0)
812 $ret .= "<ul>";
814 elsif($levels < 0)
816 $ret .= "</ul>" x abs($levels);
819 $gh->{'-mode'} = "ul";
821 $ret .= "<li>";
823 return($ret);
827 sub _ol
829 my ($gh, $levels) = @_;
830 my ($ret);
832 $ret = "";
834 if($levels > 0)
836 $ret = "<ol>";
838 elsif($levels < 0)
840 $ret = "</ol>" x abs($levels);
843 $gh->{'-mode'} = "ol";
845 $ret .= "<li>";
847 return($ret);
851 sub _blockquote
853 my ($gh) = @_;
855 $gh->_new_mode("blockquote");
856 return("\"");
860 sub _hr
862 my ($gh) = @_;
864 return("<hr size=1 noshade>");
868 sub _heading
870 my ($gh,$level,$l) = @_;
872 # creates a valid anchor
873 my ($a) = lc($l);
875 $a =~ s/[\"\']//g;
876 $a =~ s/\s/_/g;
877 $a =~ s/<[^>]+>//g;
879 $l = sprintf("<a name='%s'></a>\n<h%d class='level$level'>%s</h%d>",
880 $a, $level+$gh->{'header-offset'},
881 $l, $level+$gh->{'header-offset'});
883 return($l);
887 sub _table
889 my ($gh,$str) = @_;
891 if($gh->{'-mode'} eq "table")
893 my ($class) = "";
894 my (@spans) = $gh->_calc_col_span($str);
896 # calculate CSS class, if any
897 if($gh->{'class-oddeven'})
899 $class = ($gh->{'-tbl-row'} & 1) ? "odd" : "even";
902 $str = "<tr $class>";
904 # build columns
905 for(my $n = 0;$n < scalar(@{$gh->{'-table'}});$n++)
907 my ($i,$s);
909 $i = ${$gh->{'-table'}}[$n];
910 $i = "&nbsp;" if $i =~ /^\s*$/;
912 $s = " colspan='$spans[$n]'" if $spans[$n] > 1;
914 if($gh->{'table-headers'} and $gh->{'-tbl-row'} == 1)
916 $str .= "<th $class $s>$i</th>";
918 else
920 $str .= "<td $class $s>$i</td>";
924 $str .= "</tr>";
926 @{$gh->{'-table'}} = ();
927 $gh->{'-tbl-row'}++;
929 else
931 # new table
932 my ($params);
934 $params = "border='1'";
935 $params .= " width='100\%'" if $gh->{'expand-tables'};
936 $params .= " align='center'" if $gh->{'center-tables'};
937 $params .= " class='oddeven'" if $gh->{'class-oddeven'};
939 $gh->_new_mode("table", $params);
941 @{$gh->{'-table'}} = ();
942 $gh->{'-tbl-row'} = 1;
943 $str = "";
946 return($str);
950 ###########################################################
951 # troff Driver
953 package Grutatxt::troff;
955 @ISA = ("Grutatxt");
957 =head2 troff Driver
959 The troff driver uses the B<-me> macros and B<tbl>. A
960 good way to post-process this output (to PostScript in
961 the example) could be by using
963 groff -t -me -Tps
965 The additional parameters for a new Grutatxt object are:
967 =over 4
969 =item I<normal-size>
971 The point size of normal text. By default is 10.
973 =item I<heading-sizes>
975 This argument must be a reference to an array containing
976 the size in points of the 3 different heading levels. By
977 default, level sizes are [ 20, 18, 15 ].
979 =item I<table-type>
981 The type of table to be rendered by B<tbl>. Can be
982 I<allbox> (all lines rendered; this is the default value),
983 I<box> (only outlined) or I<doublebox> (only outlined by
984 a double line).
986 =back
988 =cut
990 sub new
992 my ($class,%args) = @_;
993 my ($gh);
995 bless(\%args,$class);
996 $gh = \%args;
998 $gh->{'-process-urls'} = 0;
1000 $gh->{'heading-sizes'} ||= [ 20, 18, 15 ];
1001 $gh->{'normal-size'} ||= 10;
1002 $gh->{'table-type'} ||= "allbox"; # box, allbox, doublebox
1004 return($gh);
1008 sub _prefix
1010 my ($gh) = @_;
1012 $gh->_push(".nr pp $gh->{'normal-size'}");
1013 $gh->_push(".nh");
1017 sub _inline
1019 my ($gh,$l) = @_;
1021 # accept only troff inlines
1022 if($l =~ /^<<\s*troff$/i)
1024 $gh->{'-inline'} = "troff";
1025 return;
1028 if($l =~ /^>>$/)
1030 delete $gh->{'-inline'};
1031 return;
1034 if($gh->{'-inline'} eq "troff")
1036 $gh->_push($l);
1041 sub _escape
1043 my ($gh,$l) = @_;
1045 $l =~ s/\\/\\\\/g;
1046 $l =~ s/^'/\\&'/;
1048 return($l);
1052 sub _empty_line
1054 my ($gh) = @_;
1056 return(".lp");
1060 sub _strong
1062 my ($gh,$str) = @_;
1063 return("\\fB$str\\fP");
1067 sub _em
1069 my ($gh,$str) = @_;
1070 return("\\fI$str\\fP");
1074 sub _funcname
1076 my ($gh,$str) = @_;
1077 return("\\fB$str\\fP");
1081 sub _varname
1083 my ($gh,$str) = @_;
1084 return("\\fI$str\\fP");
1088 sub _new_mode
1090 my ($gh,$mode,$params) = @_;
1092 if($mode ne $gh->{'-mode'})
1094 my $tag;
1096 # flush previous list
1097 if($gh->{'-mode'} eq "pre")
1099 $gh->_push(".)l");
1101 elsif($gh->{'-mode'} eq "table")
1103 chomp($gh->{'-table-head'});
1104 $gh->{'-table-head'} =~ s/\s+$//;
1105 $gh->_push($gh->{'-table-head'} . ".");
1106 $gh->_push($gh->{'-table-body'} . ".TE\n.sp 0.6");
1108 elsif($gh->{'-mode'} eq "blockquote")
1110 $gh->_push(".)q");
1113 # send new one
1114 if($mode eq "pre")
1116 $gh->_push(".(l L");
1118 elsif($mode eq "blockquote")
1120 $gh->_push(".(q");
1123 $gh->{'-mode'} = $mode;
1128 sub _dl
1130 my ($gh,$str) = @_;
1132 $gh->_new_mode("dl");
1133 return(".ip \"$str\"\n");
1137 sub _ul
1139 my ($gh) = @_;
1141 $gh->_new_mode("ul");
1142 return(".bu\n");
1146 sub _ol
1148 my ($gh) = @_;
1150 $gh->_new_mode("ol");
1151 return(".np\n");
1155 sub _blockquote
1157 my ($gh) = @_;
1159 $gh->_new_mode("blockquote");
1160 return("\"");
1164 sub _hr
1166 my ($gh) = @_;
1168 return(".hl");
1172 sub _heading
1174 my ($gh,$level,$l) = @_;
1176 $l = ".sz " . ${$gh->{'heading-sizes'}}[$level - 1] . "\n$l\n.sp 0.6";
1178 return($l);
1182 sub _table
1184 my ($gh,$str) = @_;
1186 if($gh->{'-mode'} eq "table")
1188 my ($h,$b);
1189 my (@spans) = $gh->_calc_col_span($str);
1191 # build columns
1192 $h = "";
1193 $b = "";
1194 for(my $n = 0;$n < scalar(@{$gh->{'-table'}});$n++)
1196 my ($i);
1198 if($gh->{'table-headers'} and $gh->{'-tbl-row'} == 1)
1200 $h .= "cB ";
1202 else
1204 $h .= "l ";
1207 # add span columns
1208 $h .= "s " x ($spans[$n] - 1) if $spans[$n] > 1;
1210 $b .= "#" if $n;
1212 $i = ${$gh->{'-table'}}[$n];
1213 $i =~ s/^\s+//;
1214 $i =~ s/\s+$//;
1215 $i =~ s/(\s)+/$1/g;
1216 $b .= $i;
1219 # add a separator
1220 $b .= "\n_" if $gh->{'table-headers'} and
1221 $gh->{'-tbl-row'} == 1 and
1222 $gh->{'table-type'} ne "allbox";
1224 $gh->{'-table-head'} .= "$h\n";
1225 $gh->{'-table-body'} .= "$b\n";
1227 @{$gh->{'-table'}} = ();
1228 $gh->{'-tbl-row'}++;
1230 else
1232 # new table
1233 $gh->_new_mode("table");
1235 @{$gh->{'-table'}} = ();
1236 $gh->{'-tbl-row'} = 1;
1238 $gh->{'-table-head'} = ".TS\n$gh->{'table-type'} tab (#);\n";
1239 $gh->{'-table-body'} = "";
1242 $str = "";
1243 return($str);
1247 sub _postfix
1249 my ($gh) = @_;
1251 # add to top headings and footers
1252 unshift(@{$gh->{'o'}},".ef '\%' ''");
1253 unshift(@{$gh->{'o'}},".of '' '\%'");
1254 unshift(@{$gh->{'o'}},".eh '$gh->{'-title'}' ''");
1255 unshift(@{$gh->{'o'}},".oh '' '$gh->{'-title'}'");
1259 ###########################################################
1260 # man Driver
1262 package Grutatxt::man;
1264 @ISA = ("Grutatxt::troff", "Grutatxt");
1266 =head2 man Driver
1268 The man driver is used to generate Unix-like man pages. Note that
1269 all headings have the same level with this output driver.
1271 The additional parameters for a new Grutatxt object are:
1273 =over 4
1275 =item I<section>
1277 The man page section (see man documentation). By default is 1.
1279 =item I<page-name>
1281 The name of the page. This is usually the name of the program
1282 or function the man page is documenting and will be shown in the
1283 page header. By default is the empty string.
1285 =back
1287 =cut
1289 sub new
1291 my ($class,%args) = @_;
1292 my ($gh);
1294 bless(\%args,$class);
1295 $gh = \%args;
1297 $gh->{'-process-urls'} = 0;
1299 $gh->{'section'} ||= 1;
1300 $gh->{'page-name'} ||= "";
1302 return($gh);
1306 sub _prefix
1308 my ($gh) = @_;
1310 $gh->_push(".TH \"$gh->{'page-name'}\" \"$gh->{'section'}\" \"" . localtime() . "\"");
1314 sub _inline
1316 my ($gh,$l) = @_;
1318 # accept only man markup inlines
1319 if($l =~ /^<<\s*man$/i)
1321 $gh->{'-inline'} = "man";
1322 return;
1325 if($l =~ /^>>$/)
1327 delete $gh->{'-inline'};
1328 return;
1331 if($gh->{'-inline'} eq "man")
1333 $gh->_push($l);
1338 sub _empty_line
1340 my ($gh) = @_;
1342 return(".PP");
1346 sub _new_mode
1348 my ($gh,$mode,$params) = @_;
1350 if($mode ne $gh->{'-mode'})
1352 my $tag;
1354 # flush previous list
1355 if($gh->{'-mode'} eq "pre" or
1356 $gh->{'-mode'} eq "table")
1358 $gh->_push(".fi");
1361 if($gh->{'-mode'} eq "blockquote")
1363 $gh->_push(".RE");
1366 if($gh->{'-mode'} eq "ul")
1368 $gh->_push(".RE\n" x scalar(@{$gh->{'-ul-levels'}}));
1371 if($gh->{'-mode'} eq "ol")
1373 $gh->_push(".RE\n" x scalar(@{$gh->{'-ol-levels'}}));
1376 # send new one
1377 if($mode eq "pre" or
1378 $mode eq "table")
1380 $gh->_push(".nf");
1383 if($mode eq "blockquote")
1385 $gh->_push(".RS 4");
1388 $gh->{'-mode'} = $mode;
1393 sub _dl
1395 my ($gh,$str) = @_;
1397 $gh->_new_mode("dl");
1398 return(".TP\n.B \"$str\"\n");
1402 sub _ul
1404 my ($gh, $levels) = @_;
1405 my ($ret) = "";
1407 if($levels > 0)
1409 $ret = ".RS 4\n";
1411 elsif($levels < 0)
1413 $ret = ".RE\n" x abs($levels);
1416 $gh->_new_mode("ul");
1417 return($ret . ".TP 4\n\\(bu\n");
1421 sub _ol
1423 my ($gh, $levels) = @_;
1424 my $l = @{$gh->{'-ol-levels'}};
1425 my $ret = "";
1427 $gh->{'-ol-level'} += $levels;
1429 if($levels > 0)
1431 $ret = ".RS 4\n";
1433 $l[$gh->{'-ol-level'}] = 1;
1435 elsif($levels < 0)
1437 $ret = ".RE\n" x abs($levels);
1440 $gh->_new_mode("ol");
1441 $ret .= ".TP 4\n" . $l[$gh->{'-ol-level'}]++ . ".\n";
1443 return($ret);
1447 sub _hr
1449 my ($gh) = @_;
1451 return("");
1455 sub _heading
1457 my ($gh,$level,$l) = @_;
1459 # all headers are the same depth in man pages
1460 return(".SH \"" . uc($l) . "\"");
1464 sub _table
1466 my ($gh,$str) = @_;
1468 if($gh->{'-mode'} eq "table")
1470 foreach my $r (@{$gh->{'-table-raw'}})
1472 $gh->_push("|$r|");
1475 else
1477 $gh->_new_mode("table");
1480 @{$gh->{'-table'}} = ();
1481 @{$gh->{'-table-raw'}} = ();
1483 $gh->_push($str);
1485 return("");
1489 sub _postfix
1494 =head1 AUTHOR
1496 Angel Ortega angel@triptico.com
1498 =cut