Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- use strict;
- use HTML::Entities;
- =begin comment
- This is a DFA that parses plain text tables as input and produces
- HTML tables as output. Input lines not recognized as part of a
- table are merely echoed.
- A Table consists of the follwing parts:
- Title/Caption [Optional]
- Starts with at least three = characters, followed by any number of
- lines of text, followed by another line of at least three = characters.
- Header
- One or more ColHeaders can occur across one (or more) lines immediately
- following the Title.
- Simple Column Header
- This Header is any text not broken by two white spaces.
- Two or more white spaces signifies a column break.
- ColSpan Column Header
- This Header can span multiple columns.
- If Colspan Column Headers are used, then the table should be more-or-
- less fixed-width, so that Cells line-up underneath their respective
- Column Headers, otherwise there's no guarantee the table will parse
- correctly.
- Headers are given the styles "first" or "other" in the HTML output, and
- "first" merely indicates the first column.
- Body
- An table can contain one or more Bodies.
- Odd and even Bodies are given odd and even styles in the HTML output.
- A Body contains at least one line of Content.
- The start of a Body is indicated by a line of at least three -
- characters immediately followed by a line of Content.
- Content is organised into Rows (number of Cols per row is fixed).
- Rows
- Each Body has at least one Row.
- Odd and even Rows are given odd and even styles in the HTML output.
- Rows each contain one Cell per Col.
- Cells contain any text not broken by two white spaces.
- Cells MUST contain some text. Use a dot "." for an otherwise empty cell.
- Two (or more) white spaces signify a column break.
- Cells are given the styles "object", "quantity", "value", "range" or "dot".
- Object is text, a quantity is a number optionally followed by a unit of
- measure, a value is a positive integer, a range is two values separated by
- a hyphen, and dot is a "." indicating an empty cell.
- Content is concluded by a line of at least three - characters
- immediately followed by (optional) Footnotes, then a blank line.
- Footnotes [Otional]
- These follow immediately after the (last) Body. It consists of one or
- more asteristsk, a space, and some text, and ends with a line break.
- There can be any number of Footnotes.
- Here is an example of an plain text table used as input:
- ===================================================
- Missile Range
- ===================================================
- Item Short Medium Long Maximum*
- ---------------------------------------------------
- Crossbow 60ft 120ft 180ft 180yd
- Dagger, hurled . 30ft . .
- Hand axe, hurled . 30ft . .
- Longbow 70ft 140ft 210ft 120yd
- ---------------------------------------------------
- * Applicable to massed missile fire outdoors
- ...
- The motivation behind this program is to add more sophisticated table
- processing to the "markdown" language by John Gruber. See
- L<Markdown|http://daringfireball.net/projects/markdown/>
- ...
- Since this is an early draft of the program, it produces a lot of
- debugging information to STDERR. To redirect STDOUT and STDERR to
- separate files, use a command-line like the following:
- perl table-parser.pl "heading rowcolspan empty.txt" >table.html 2>debug.txt
- On Windows, to supress STDERR, simply redirect it to the nul device like so:
- perl table-parser.pl "heading rowcolspan empty.txt" >table.html 2>nul
- =end comment
- =cut
- our $STATE = "TEXT"; # := TEXT | TITLE | HEADER | BODY | FOOTER
- our $n = -1; # line number; 0-based
- our @lines = <>; # array containing each line of input file
- our $m = $#lines; # max lines
- our %header = (); # hashtable containing number of columns in each row of header, indexed by line number
- our %body = (); # hashtable containing number of columns in each row of body, indexed by line number
- our @cell = (); # matrix describing table cell $cell[r][c][x] r=line,c=col,x=0:startpos,1:endpos,3:data
- our @colpos = (); # list describing coalesced column structure $colpos[c][x] with c & x as above
- our @colspanempty = (); # list of lines in header with fewer than maxcol columns
- our $body=0; # counter for multiple bodies
- our $maxcol=0; # maximum number of columns in a table row
- our @caption=(); # Title/caption
- our @footer=(); # Footer
- for $_ (@lines) {
- chomp;
- ++$n;
- # STATE transitions
- if(/^\s*$/ && $STATE ne "TEXT"){ $STATE="TEXT"; table(); next;}
- if(/^===+/ && $STATE eq "TEXT" ){ $STATE="TITLE"; next;}
- if(/^===+/ && $STATE eq "TITLE" ){ $STATE="HEADER"; $maxcol=0; next;}
- if(/^---+/ && $STATE eq "HEADER"){ $STATE="BODY"; $body = 1; next;}
- if(/^---+/ && $STATE eq "TEXT"){
- if($n!=0 && $lines[$n-1] !~ /^\s*$/) {
- # "Basic" table
- # Previous line was the header! Go back & get it.
- my $col=0;
- while($lines[$n-1]=~/(\S+(?:\s\S+)*)(\s\s+|$)/g ){
- ++$col; if($col>$maxcol){$maxcol=$col;}
- $cell[$n-1][$col][0] = $-[1];
- $cell[$n-1][$col][1] = $+[1];
- $cell[$n-1][$col][3] = $1;
- }
- $header{$n-1} = $col;
- }
- $STATE="BODY"; $body = 1; $maxcol=0; next;
- }
- if(/^---+/ && $STATE eq "BODY" ){
- if( ($n==$m) || ($n<$m && $lines[$n+1] =~ /^\s*$/) ) {$STATE="TEXT";table();next;}
- elsif( $lines[$n+1] =~ /^(\*)+\s\S+/ ) {$STATE="FOOTER";next;}
- else {$STATE="BODY"; $body++; next;}
- }
- if($STATE ne "BODY") { print STDERR $n, " ", $STATE, ": ", $_, "\n"; } # DEBUG
- if($STATE eq "TEXT" && ($n==$m || $lines[$n+1] !~ /^---+/)) { print $_, "\n"; }
- if($STATE eq "TITLE") { push @caption, trim($_); }
- if($STATE eq "FOOTER") { push @footer, trim($_); }
- if($STATE eq "BODY") {
- print STDERR "line ", $n, "\n";
- my $col = 0;
- while(/(\S+(?:\s\S+)*)(\s\s+|$)/g ){
- ++$col; if($col>$maxcol){$maxcol=$col;}
- $cell[$n][$col][0] = $-[1];
- $cell[$n][$col][1] = $+[1];
- $cell[$n][$col][3] = $1;
- if($col==1){ $cell[$n][$col][4] = $body; }
- print STDERR " column ", $col," from ", $-[1], " to ", $+[1],
- ": ", $&,
- "\n";
- }
- $body{$n} = $col;
- }
- if($STATE eq "HEADER") {
- print STDERR "line ", $n, "\n";
- my $col=0;
- while(/(\S+(?:\s\S+)*)(\s\s+|$)/g ){
- ++$col; if($col>$maxcol){$maxcol=$col;}
- $cell[$n][$col][0] = $-[1];
- $cell[$n][$col][1] = $+[1];
- $cell[$n][$col][3] = $1;
- print STDERR " column ", $col," from ", $-[1], " to ", $+[1],
- ": ", $&,
- "\n";
- }
- $header{$n} = $col;
- }
- }
- sub table {
- validate();
- start();
- caption();
- head();
- body();
- foot();
- end();
- reset_dfa();
- }
- sub validate {
- # Problem rows have number of columns != maxcols
- @colspanempty = ();
- foreach my $k (sort {$a<=>$b} keys(%header)){
- print STDERR "header on line $k has $header{$k} columns\n";
- if($header{$k}!=$maxcol) { push @colspanempty, $k; }
- }
- foreach my $k (sort {$a<=>$b} keys(%body)){
- print STDERR "row on line $k has $body{$k} columns\n";
- if($body{$k}!=$maxcol) { die "malformed row on line $k has $body{$k} columns should be $maxcol\n";}
- }
- }
- sub head {
- if(scalar @colspanempty){
- head_colspan();
- } else {
- head_simple();
- }
- }
- sub head_colspan {
- coalesce();
- print "<thead>\n";
- foreach my $r (sort {$a <=> $b} keys(%header)) {
- print "<tr>\n";
- my $c = 1;
- my $h = 1;
- my $span = 1;
- while($c <= $maxcol && $h <= $header{$r}) {
- if(overlaps($colpos[$c][0],$colpos[$c][1],$cell[$r][$h][0],$cell[$r][$h][1])){
- my $startcol = $c;
- while(++$c <= $maxcol && overlaps($colpos[$c][0],$colpos[$c][1],$cell[$r][$h][0],$cell[$r][$h][1])){
- $span++;
- }
- print_header($cell[$r][$h][3],$startcol,$span);
- $h++;
- $span = 1;
- } else { # no overlap
- print_header("",$c,1);
- $c++;
- }
- }
- if($c<=$maxcol && $h>$header{$r}){
- while($c++<=$maxcol) {
- print_header("",$c,1);
- }
- }
- print "</tr>\n";
- }
- print "</thead>\n";
- }
- sub start {
- print "<table>\n";
- }
- sub caption {
- if(scalar @caption){
- my $caption = join " ", @caption;
- print "<caption>", encode_entities($caption), "</caption>\n";
- }
- }
- sub head_simple {
- print "<thead>\n",
- "<tr>\n";
- foreach my $r (sort {$a <=> $b} keys(%header)) {
- foreach my $c (1..$maxcol) {
- print_header($cell[$r][$c][3],$c,1);
- }
- }
- print "</tr>\n",
- "</thead>\n";
- }
- sub body {
- my $s = -1; # row number within current body number
- my $t = -1; # tbody number
- foreach my $r (sort {$a <=> $b} keys(%body)) {
- if($t!=$cell[$r][1][4]) {
- if($t!=-1){ print "</tbody>\n"; }
- $t = $cell[$r][1][4];
- $s=0;
- print "<tbody style=\"", $t%2==1?"odd":"even", "\">\n";
- }
- $s++;
- print "<tr style=\"", $s%2==1?"odd":"even", "\">\n";
- foreach my $c (1..$maxcol) {
- my $data = $cell[$r][$c][3];
- my $style = style($data);
- print "<td style=\"$style\">", encode_entities($data), "</td>\n";
- }
- print "</tr>\n";
- }
- print "</tbody>\n";
- }
- sub foot {
- if(scalar @footer){
- print "<tfoot>\n";
- foreach my $f (@footer) {
- print "<tr>\n",
- "<td colspan=\"$maxcol\">", encode_entities($f), "</td>\n",
- "</tr>\n";
- }
- print "</tfoot>\n";
- }
- }
- sub end {
- print "</table>\n";
- }
- sub coalesce {
- # This subroutine looks at rows that have maxcol cells
- # and uses the data in these cells to establish the minimum
- # positions/widths in each column. It's merely a guess,
- # and other possibilities exist.
- @colpos = ();
- for my $c (1..$maxcol) {
- $colpos[$c][0] = 1_000_000;
- $colpos[$c][1] = -1;
- }
- foreach my $r (keys %header) {
- next unless $header{$r} == $maxcol;
- for my $c (1..$maxcol) {
- print STDERR "cell[$r][$c] = (",
- $cell[$r][$c][0], ", ",
- $cell[$r][$c][1], ", ",
- $cell[$r][$c][3], ")\n";
- if( $cell[$r][$c][0] < $colpos[$c][0] ){
- $colpos[$c][0] = $cell[$r][$c][0];
- }
- if( $cell[$r][$c][1] > $colpos[$c][1] ){
- $colpos[$c][1] = $cell[$r][$c][1];
- }
- }
- }
- foreach my $r (keys %body) {
- for my $c (1..$maxcol) {
- print STDERR "cell[$r][$c] = (", $cell[$r][$c][0], ", ",
- $cell[$r][$c][1], ", ",
- $cell[$r][$c][3], ")\n";
- if( $cell[$r][$c][0] < $colpos[$c][0] ){
- $colpos[$c][0] = $cell[$r][$c][0];
- }
- if( $cell[$r][$c][1] > $colpos[$c][1] ){
- $colpos[$c][1] = $cell[$r][$c][1];
- }
- }
- }
- # print colpos
- for my $c (1..$maxcol) {
- print STDERR "column $c starts at ",
- $colpos[$c][0], " and ends at ",
- $colpos[$c][1], "\n";
- }
- }
- sub overlaps {
- =begin comment
- Does column a overlap column b?
- Imagine a is made of minuses and b plusses. There are 7 cases:
- minus coincides plus:
- -----
- +++++
- minus before plus:
- -----
- +++++
- minus after plus:
- -----
- +++++
- minus overlaps start of plus:
- -----
- +++++
- minus overlaps end of plus:
- -----
- +++++
- minus contains plus:
- -----
- +++
- minus contained by plus:
- ---
- +++++
- =end comment
- =cut
- my ($a1,$a2,$b1,$b2) = @_;
- if( ($a1 >= $b1 && $a1 <= $b2) # overlaps end
- || ($a2 >= $b1 && $a2 <= $b2) # overlaps start
- || ($a1 <= $b1 && $a2 >= $b2) # contains, coincides
- || ($a1 >= $b1 && $a2 <= $b2) # contained by, coincides
- ){
- 1;
- } else {
- 0;
- }
- }
- sub print_header {
- my ($str,$c,$span) = @_;
- my $style = ($c == 1) ? "first" : "other";
- print "<th style=\"$style\"";
- if($span != 1){ print " colspan=\"$span\""; }
- print ">", encode_entities($str), "</th>\n";
- }
- sub trim {
- my ($str) = @_;
- $str =~ s/^\s+//;
- $str =~ s/\s+$//;
- return $str;
- }
- sub style {
- my ($str) = @_;
- if($str eq "."){ return "dot"; }
- if($str eq "0"){ return "quantity"; }
- if($str =~ /^\d+\s?-\s?\d+$/){ return "range"; }
- if($str =~ /^\d+(\+\d+)?$/){ return "value"; }
- if($str =~ /^\d+\D/){ return "quantity"; }
- return "object";
- }
- sub reset_dfa {
- %header = ();
- %body = ();
- @cell = ();
- @colpos = ();
- @colspanempty = ();
- $body=0;
- $maxcol=0;
- @caption=();
- @footer=();
- }
Advertisement
Add Comment
Please, Sign In to add comment