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 #####################################################################
35 Grutatxt - Text to HTML (and other formats) converter
41 # create a new Grutatxt converter object
42 $grutatxt=new Grutatxt();
44 # process a Grutatxt format string
45 @output=$grutatxt->process($text);
48 @output2=$grutatxt->process_file($file);
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
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
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
94 Output format. Can be HTML, troff or man. HTML is used if not specified.
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.
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.
111 If I<index> is specified as a reference to array, it will
112 be filled with strings in the format
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.
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.
149 my ($class,%args) = @_;
152 $args{'mode'} ||= 'HTML';
154 $class .= "::" . $args{'mode'};
156 $gh = new
$class(%args);
163 # escapes special characters, ignoring passthrough code
167 # splits between << and >>
168 my (@l) = split(/(<<|>>)/, $l);
173 # escape only text outside << and >>
174 unless($l eq "<<" .. $l eq ">>")
176 $l = $gh->_escape($l);
182 # join again, stripping << and >>
183 $l = join('', grep(!/^(<<|>>)$/, @l));
191 @output=$grutatxt->process($text);
193 Processes a text in Grutatxt format. The result is returned
194 as an array of lines.
200 my ($gh,$content) = @_;
206 # clean title and paragraph numbers
207 $gh->{'-title'} = "";
211 @
{$gh->{'marks'}} = () if ref($gh->{'marks'});
214 @
{$gh->{'index'}} = () if ref($gh->{'index'});
216 # reset abstract line
217 ${$gh->{'abstract'}} = 0 if ref($gh->{'abstract'});
222 $gh->{'-mode'} = undef;
224 foreach my $l (split(/\n/,$content))
226 # inline data (passthrough)
227 if($l =~ /^<<$/ .. $l =~ /^>>$/)
234 if($l =~ /^\s*<\->\s*$/)
236 push(@
{$gh->{'marks'}},scalar(@
{$gh->{'o'}}))
237 if ref($gh->{'marks'});
242 # escape possibly dangerous characters
243 $l = $gh->escape($l);
247 if($l =~ s/^$/$gh->_empty_line()/ge)
249 # mark the abstract end
254 # mark abstract if it's the
255 # second paragraph from the title
256 ${$gh->{'abstract'}} = scalar(@
{$gh->{'o'}})-1
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;
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;
309 $l =~ s/(\$[\w_\.]+)/$gh->_varname($1)/ge;
317 if($l =~ s/^\s\*\s+([\w\s\-\(\)]+)\:\s+/$gh->_dl($1)/e)
319 $gh->{'-mode-elems'} ++;
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'} ++;
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'} ++;
339 elsif($l =~ s/^\s\"/$gh->_blockquote()/e)
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)
355 elsif($l =~ s/^(\s.*)$/$gh->_pre($1)/e)
362 # back to normal mode
363 $gh->_new_mode(undef);
367 $l =~ s/^(=+)\s*$/$gh->_process_heading(1,$1)/e;
370 $l =~ s/^(-+)\s*$/$gh->_process_heading(2,$1)/e;
373 $l =~ s/^(~+)\s*$/$gh->_process_heading(3,$1)/e;
375 # change ------ into hr
376 $l =~ s/^----*$/$gh->_hr()/e;
379 $gh->_push($l) if $l;
383 $gh->_new_mode(undef);
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.
411 open F
, $file or return(undef);
413 my ($content) = join('',<F
>);
416 return($gh->process($content));
424 push(@
{$gh->{'o'}},$l);
430 my ($gh,$level,$hd) = @_;
433 $l = pop(@
{$gh->{'o'}});
435 if($l eq $gh->_empty_line())
442 $gh->{'-title'} = $l if $level == 1 and not $gh->{'-title'};
445 if(ref($gh->{'index'}))
447 push(@
{$gh->{'index'}},"$level,$l");
450 return($gh->_heading($level,$l));
459 # strip first + and all -
463 my ($t) = 1; @spans = ();
464 for(my $n = 0;$n < length($l);$n++)
466 if(substr($l,$n,1) eq '+')
473 # it's a colspan mark:
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);
504 # if any other mode is active, add to it
505 if($gh->{'-mode'} and $gh->{'-mode'} ne "pre")
509 my ($a) = pop(@
{$gh->{'o'}})." ".$l;
515 $gh->_new_mode("pre");
524 my ($gh, $str, $ind) = @_;
533 # if last level is less indented, increase
540 # if last level is more indented, decrease
541 # levels until the same is found (or back to
542 # the beginning if not)
546 last if $l[-1] == $ind;
560 return($gh->_ul($gh->_multilevel_list('-ul-levels', $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
599 ###########################################################
602 package Grutatxt
::HTML
;
608 The additional parameters for a new Grutatxt object are:
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%).
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
649 my ($class,%args) = @_;
652 bless(\
%args,$class);
655 $gh->{'-process-urls'} = 1;
665 # accept unnamed and HTML inlines
666 if($l =~ /^<<$/ or $l =~ /^<<\s*html$/i)
668 $gh->{'-inline'} = "HTML";
674 delete $gh->{'-inline'};
678 if($gh->{'-inline'} eq "HTML")
707 my ($gh,$url,$label) = @_;
709 $label = $url unless $label;
711 return("<a href=\"$url\">$label</a>");
718 return("<strong class='strong'>$str</strong>");
725 return("<em class='em'>$str</em>");
732 return("<code class='funcname'>$str</code>");
739 return("<code class='var'>$str</code>");
745 my ($gh,$mode,$params) = @_;
747 if($mode ne $gh->{'-mode'})
751 # flush previous mode
752 $gh->_push($gh->{'-mode-elem-close'})
753 if $gh->{'-mode-elem-close'} and $gh->{'-mode-elems'};
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'}>");
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;
788 if($gh->{'dl-as-dl'})
790 $gh->_new_mode("dl");
791 $ret .= "<dt><strong class='term'>$str</strong><dd>";
795 $gh->_new_mode("table");
796 $ret .= "<tr><td valign='top'><strong class='term'>$1</strong> </td><td valign='top'>";
805 my ($gh, $levels) = @_;
816 $ret .= "</ul>" x
abs($levels);
819 $gh->{'-mode'} = "ul";
829 my ($gh, $levels) = @_;
840 $ret = "</ol>" x
abs($levels);
843 $gh->{'-mode'} = "ol";
855 $gh->_new_mode("blockquote");
864 return("<hr size=1 noshade>");
870 my ($gh,$level,$l) = @_;
872 # creates a valid anchor
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'});
891 if($gh->{'-mode'} eq "table")
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>";
905 for(my $n = 0;$n < scalar(@
{$gh->{'-table'}});$n++)
909 $i = ${$gh->{'-table'}}[$n];
910 $i = " " 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>";
920 $str .= "<td $class $s>$i</td>";
926 @
{$gh->{'-table'}} = ();
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;
950 ###########################################################
953 package Grutatxt
::troff
;
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
965 The additional parameters for a new Grutatxt object are:
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 ].
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
992 my ($class,%args) = @_;
995 bless(\
%args,$class);
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
1012 $gh->_push(".nr pp $gh->{'normal-size'}");
1021 # accept only troff inlines
1022 if($l =~ /^<<\s*troff$/i)
1024 $gh->{'-inline'} = "troff";
1030 delete $gh->{'-inline'};
1034 if($gh->{'-inline'} eq "troff")
1063 return("\\fB$str\\fP");
1070 return("\\fI$str\\fP");
1077 return("\\fB$str\\fP");
1084 return("\\fI$str\\fP");
1090 my ($gh,$mode,$params) = @_;
1092 if($mode ne $gh->{'-mode'})
1096 # flush previous list
1097 if($gh->{'-mode'} eq "pre")
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")
1116 $gh->_push(".(l L");
1118 elsif($mode eq "blockquote")
1123 $gh->{'-mode'} = $mode;
1132 $gh->_new_mode("dl");
1133 return(".ip \"$str\"\n");
1141 $gh->_new_mode("ul");
1150 $gh->_new_mode("ol");
1159 $gh->_new_mode("blockquote");
1174 my ($gh,$level,$l) = @_;
1176 $l = ".sz " . ${$gh->{'heading-sizes'}}[$level - 1] . "\n$l\n.sp 0.6";
1186 if($gh->{'-mode'} eq "table")
1189 my (@spans) = $gh->_calc_col_span($str);
1194 for(my $n = 0;$n < scalar(@
{$gh->{'-table'}});$n++)
1198 if($gh->{'table-headers'} and $gh->{'-tbl-row'} == 1)
1208 $h .= "s " x
($spans[$n] - 1) if $spans[$n] > 1;
1212 $i = ${$gh->{'-table'}}[$n];
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'}++;
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'} = "";
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 ###########################################################
1262 package Grutatxt
::man
;
1264 @ISA = ("Grutatxt::troff", "Grutatxt");
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:
1277 The man page section (see man documentation). By default is 1.
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.
1291 my ($class,%args) = @_;
1294 bless(\
%args,$class);
1297 $gh->{'-process-urls'} = 0;
1299 $gh->{'section'} ||= 1;
1300 $gh->{'page-name'} ||= "";
1310 $gh->_push(".TH \"$gh->{'page-name'}\" \"$gh->{'section'}\" \"" . localtime() . "\"");
1318 # accept only man markup inlines
1319 if($l =~ /^<<\s*man$/i)
1321 $gh->{'-inline'} = "man";
1327 delete $gh->{'-inline'};
1331 if($gh->{'-inline'} eq "man")
1348 my ($gh,$mode,$params) = @_;
1350 if($mode ne $gh->{'-mode'})
1354 # flush previous list
1355 if($gh->{'-mode'} eq "pre" or
1356 $gh->{'-mode'} eq "table")
1361 if($gh->{'-mode'} eq "blockquote")
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'}}));
1377 if($mode eq "pre" or
1383 if($mode eq "blockquote")
1385 $gh->_push(".RS 4");
1388 $gh->{'-mode'} = $mode;
1397 $gh->_new_mode("dl");
1398 return(".TP\n.B \"$str\"\n");
1404 my ($gh, $levels) = @_;
1413 $ret = ".RE\n" x
abs($levels);
1416 $gh->_new_mode("ul");
1417 return($ret . ".TP 4\n\\(bu\n");
1423 my ($gh, $levels) = @_;
1424 my $l = @
{$gh->{'-ol-levels'}};
1427 $gh->{'-ol-level'} += $levels;
1433 $l[$gh->{'-ol-level'}] = 1;
1437 $ret = ".RE\n" x
abs($levels);
1440 $gh->_new_mode("ol");
1441 $ret .= ".TP 4\n" . $l[$gh->{'-ol-level'}]++ . ".\n";
1457 my ($gh,$level,$l) = @_;
1459 # all headers are the same depth in man pages
1460 return(".SH \"" . uc($l) . "\"");
1468 if($gh->{'-mode'} eq "table")
1470 foreach my $r (@
{$gh->{'-table-raw'}})
1477 $gh->_new_mode("table");
1480 @
{$gh->{'-table'}} = ();
1481 @
{$gh->{'-table-raw'}} = ();
1496 Angel Ortega angel@triptico.com