2009-07-30 10:07:49 -04:00
#!/usr/bin/perl
2009-07-30 20:34:36 -04:00
# import module
use Getopt::Long;
2015-07-31 22:55:24 -04:00
use File::Basename;
use File::Spec;
2009-07-30 20:34:36 -04:00
$semicolon = ";"; # help out stupid emacs
2009-07-31 16:26:15 -04:00
$title = "Ardour Shortcuts";
2009-07-30 10:07:49 -04:00
$in_group_def = 0;
$group_name;
$group_text;
$group_key;
2009-07-30 20:34:36 -04:00
$group_number = 0;
2009-07-30 10:07:49 -04:00
%group_names;
%group_text;
2015-07-31 22:55:24 -04:00
%owner_bindings;
%group_owners;
2009-07-30 20:34:36 -04:00
%group_bindings;
%modifier_map;
%group_numbering;
2009-07-31 16:26:15 -04:00
%merge_bindings;
2009-07-30 20:34:36 -04:00
$platform = linux;
2014-07-07 16:34:20 -04:00
$winkey = 'Win';
2016-06-22 18:56:35 -04:00
$make_cheatsheet = 0;
$make_accelmap = 1;
2009-07-31 16:26:15 -04:00
$merge_from = "";
2012-12-26 11:44:09 -05:00
$html = 0;
2009-07-30 20:34:36 -04:00
2009-07-31 16:26:15 -04:00
GetOptions ("platform=s" => \$platform,
"winkey=s" => \$winkey,
2021-03-24 18:33:18 -04:00
"cheatsheet=i" => \$make_cheatsheet,
"accelmap=i" => \$make_accelmap,
2012-12-26 11:44:09 -05:00
"merge=s" => \$merge_from,
2021-03-24 18:33:18 -04:00
"html=i" => \$html);
2009-07-30 20:34:36 -04:00
2012-12-10 17:16:27 -05:00
if ($platform eq "darwin") {
2009-07-31 16:26:15 -04:00
2015-01-28 21:02:57 -05:00
$gtk_modifier_map{'PRIMARY'} = 'Primary'; # GTK supports Primary to allow platform-independent binding to the "primary" modifier, which on OS X is Command
2011-11-07 14:19:31 -05:00
$gtk_modifier_map{'SECONDARY'} = 'Control';
2009-07-30 20:34:36 -04:00
$gtk_modifier_map{'TERTIARY'} = 'Shift';
2015-01-28 21:02:57 -05:00
$gtk_modifier_map{'LEVEL4'} = 'Mod1';
# cs_modifier_map == "Cheat Sheet Modifier Map"
# Used to control what gets shown in the
# cheat sheet for a given (meta)-modifier
2009-07-30 20:34:36 -04:00
2012-12-01 20:45:31 -05:00
$cs_modifier_map{'PRIMARY'} = 'Cmd';
2021-03-24 18:33:18 -04:00
$cs_modifier_map{'SECONDARY'} = 'Ctrl';
2009-07-30 20:34:36 -04:00
$cs_modifier_map{'TERTIARY'} = 'Shift';
2015-01-28 21:02:57 -05:00
$cs_modifier_map{'LEVEL4'} = 'Opt';
2009-07-31 16:26:15 -04:00
2015-01-28 21:02:57 -05:00
# used to display what gets shown in the
# cheat sheet for mouse bindings. Differs
# from cs_modifier map in using shorter
# abbreviations.
2009-07-31 16:26:15 -04:00
$mouse_modifier_map{'PRIMARY'} = 'Cmd';
2011-11-07 14:19:31 -05:00
$mouse_modifier_map{'SECONDARY'} = 'Ctrl';
2009-07-31 16:26:15 -04:00
$mouse_modifier_map{'TERTIARY'} = 'Shift';
2011-11-07 14:19:31 -05:00
$mouse_modifier_map{'LEVEL4'} = 'Opt';
2009-07-30 20:34:36 -04:00
} else {
$gtk_modifier_map{'PRIMARY'} = 'Control';
$gtk_modifier_map{'SECONDARY'} = 'Alt';
$gtk_modifier_map{'TERTIARY'} = 'Shift';
2015-01-28 21:02:57 -05:00
$gtk_modifier_map{'LEVEL4'} = $winkey; # something like "Mod4><Super"
# cs_modifier_map == "Cheat Sheet Modifier Map"
# Used to control what gets shown in the
# cheat sheet for a given (meta)-modifier
2009-07-30 20:34:36 -04:00
$cs_modifier_map{'PRIMARY'} = 'Control';
$cs_modifier_map{'SECONDARY'} = 'Alt';
$cs_modifier_map{'TERTIARY'} = 'Shift';
2015-01-28 21:02:57 -05:00
$cs_modifier_map{'LEVEL4'} = 'Win';
# used to display what gets shown in the
# cheat sheet for mouse bindings. Differs
# from cs_modifier map in using shorter
# abbreviations.
2009-07-31 16:26:15 -04:00
$mouse_modifier_map{'PRIMARY'} = 'Ctl';
$mouse_modifier_map{'SECONDARY'} = 'Alt';
$mouse_modifier_map{'TERTIARY'} = 'Shift';
2015-01-28 21:02:57 -05:00
$mouse_modifier_map{'LEVEL4'} = 'Win';
2009-07-30 20:34:36 -04:00
}
2021-03-24 18:33:18 -04:00
$html_modifier_map{'PRIMARY'} = '1';
$html_modifier_map{'SECONDARY'} = '2';
$html_modifier_map{'TERTIARY'} = '3';
$html_modifier_map{'LEVEL4'} = '4';
2012-12-26 11:44:09 -05:00
%keycodes = ();
if ($html) {
%keycodes = (
'asciicircum' => '^',
'apostrophe' => '\'',
'bracketleft' => '[',
'bracketright' => ']',
'braceleft' => '{',
'braceright' => '}',
'backslash' => '\\',
'slash' => '/',
'rightanglebracket' => '>',
'leftanglebracket' => '<',
'ampersand' => '&',
'comma' => ',',
'period' => '.',
'semicolon' => ';',
'colon' => ':',
'equal' => '=',
'minus' => '-',
'plus' => '+',
'grave' => '`',
'rightarrow' => '→',
'leftarrow' => '←',
'uparrow' => '↑',
'downarrow' => '↓',
'Page_Down' => 'PageDown',
'Page_Up' => 'PageUp',
'space' => 'space',
'KP_Right' => 'KP-→',
'KP_Left' => 'KP-←',
'KP_Up' => 'KP-↑',
'KP_Down' => 'KP-↓',
'KP_0' => 'KP-0;',
'greater' => '>',
'less' => '<',
2021-03-24 18:33:18 -04:00
'ISO_Left_Tab' => 'Tab',
'nabla' => 'Tab',
2012-12-26 11:44:09 -05:00
);
} else {
%keycodes = (
'asciicircum' => '\\verb=^=',
'apostrophe' => '\'',
'bracketleft' => '[',
'bracketright' => ']',
'braceleft' => '\\{',
'braceright' => '\\}',
'backslash' => '$\\backslash$',
'slash' => '/',
'rightanglebracket' => '>',
'leftanglebracket' => '<',
'ampersand' => '\\&',
'comma' => ',',
'period' => '.',
'semicolon' => ';',
'colon' => ':',
'equal' => '=',
'minus' => '-',
'plus' => '+',
'grave' => '`',
'rightarrow' => '$\rightarrow$',
'leftarrow' => '$\\leftarrow$',
'uparrow' => '$\\uparrow$',
'downarrow' => '$\\downarrow$',
'Page_Down' => 'Page Down',
'Page_Up' => 'Page Up',
'space' => 'space',
'KP_' => 'KP$\_$',
'greater' => '>',
'less' => '<',
2009-07-30 20:34:36 -04:00
);
2012-12-26 11:44:09 -05:00
}
2009-07-30 10:07:49 -04:00
2009-07-31 16:26:15 -04:00
if ($merge_from) {
open (BINDINGS, $merge_from) || die ("merge from bindings: file not readable");
while (<BINDINGS>) {
next if (/^$semicolon/);
if (/^\(gtk_accel/) {
chop; # newline
chop; # closing parenthesis
s/"//g;
($junk, $action, $binding) = split;
$merge_bindings{$action} = $binding;
}
}
close (BINDINGS);
}
2015-07-31 22:55:24 -04:00
if ($make_accelmap && !$merge_from) {
print "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
2009-07-31 16:26:15 -04:00
}
2015-07-31 22:55:24 -04:00
$bindings_name = basename ($ARGV[0]);
$bindings_name =~ s/.bindings\.in$//;
open SOURCE, "<", $ARGV[0] or die $!;
while (<SOURCE>) {
2009-07-30 20:34:36 -04:00
next if /^$semicolon/;
2009-07-30 10:07:49 -04:00
2009-07-31 16:26:15 -04:00
if (/^\$/) {
s/^\$//;
$title = $_;
next;
}
2009-07-30 10:07:49 -04:00
if (/^%/) {
2009-07-30 20:34:36 -04:00
2009-07-30 10:07:49 -04:00
if ($in_group_def) {
chop $group_text;
$group_names{$group_key} = $group_name;
$group_text{$group_key} = $group_text;
2009-07-30 20:34:36 -04:00
$group_numbering{$group_key} = $group_number;
# each binding entry is 2 element array. bindings
# are all collected into a container array. create
# the first dummy entry so that perl knows what we
# are doing.
$group_bindings{$group_key} = [ [] ];
2009-07-30 10:07:49 -04:00
}
s/^%//;
chop;
2015-07-31 22:55:24 -04:00
($group_key,$owner,$group_name) = split (/\s+/, $_, 3);
if ($make_accelmap) {
if (!exists ($owner_bindings{$owner})) {
$owner_bindings{$owner} = [ [] ];
2015-07-11 09:17:59 -04:00
}
2015-07-31 22:55:24 -04:00
$group_owners{$group_key} = $owner;
2015-07-11 09:17:59 -04:00
}
2009-07-30 20:34:36 -04:00
$group_number++;
2009-07-30 10:07:49 -04:00
$group_text = "";
$in_group_def = 1;
next;
}
if ($in_group_def) {
2009-07-30 20:34:36 -04:00
if (/^@/) {
chop $group_text;
$group_names{$group_key} = $group_name;
$group_text{$group_key} = $group_text;
$in_group_def = 0;
} else {
next if (/^[ \t]+$/);
$group_text .= $_;
$group_text;
next;
}
}
if (/^@/) {
s/^@//;
chop;
($key,$action,$binding,$text) = split (/\|/, $_, 4);
2021-03-24 18:33:18 -04:00
# do not include "alt-" or "alternate-" actions in the HTML output
if ($html && $action =~ /\/alt/) {
next;
}
2015-07-11 09:17:59 -04:00
$gkey = $key;
$gkey =~ s/^-//;
2015-07-31 22:55:24 -04:00
$owner = $group_owners{$gkey};
2009-07-30 20:34:36 -04:00
# substitute bindings
$gtk_binding = $binding;
2009-07-31 16:26:15 -04:00
if ($merge_from) {
$lookup = "<Actions>/" . $action;
if ($merge_bindings{$lookup}) {
$binding = $merge_bindings{$lookup};
} else {
2009-07-31 16:33:58 -04:00
if ($key =~ /^\+/) {
# forced inclusion of bindings from template
} else {
# this action is not defined in the merge from set, so forget it
next;
}
2009-07-31 16:26:15 -04:00
}
}
2009-07-30 20:34:36 -04:00
2016-06-22 18:56:35 -04:00
# store the accelmap output
2009-07-30 20:34:36 -04:00
if ($key =~ /^\+/) {
# remove + and don't print it in the accelmap
$key =~ s/^\+//;
} else {
2015-07-31 22:55:24 -04:00
# include this in the accelmap if it is part of a group that has an "owner"
if (!$merge_from && $make_accelmap && exists ($owner_bindings{$owner})) {
$b = $binding;
$b =~ s/<@//g;
$b =~ s/@>//g;
$b =~ s/PRIMARY/Primary-/;
$b =~ s/SECONDARY/Secondary-/;
$b =~ s/TERTIARY/Tertiary-/;
$b =~ s/LEVEL4/Level4-/;
2016-06-23 08:37:12 -04:00
$g = $group_names{$gkey};
$g =~ s/\\&/&/g;
2015-07-31 22:55:24 -04:00
$bref = $owner_bindings{$owner};
2016-06-23 08:37:12 -04:00
push (@$bref, [ $action, $b, $g]);
2009-07-31 16:26:15 -04:00
}
2009-07-30 20:34:36 -04:00
}
if ($key =~ /^-/) {
# do not include this binding in the cheat sheet
next;
}
$bref = $group_bindings{$key};
push (@$bref, [$binding, $text]);
2016-06-22 18:56:35 -04:00
$sref = $section_text{$key};
push (@$sref, [$owner]);
2009-07-30 10:07:49 -04:00
next;
}
next;
}
2015-07-31 22:55:24 -04:00
if ($make_accelmap) {
print "<BindingSet name=\"" . $bindings_name . "\">\n";
2020-01-20 10:59:00 -05:00
foreach $owner (sort keys %owner_bindings) {
2015-08-01 21:36:06 -04:00
print " <Bindings name=\"$owner\">\n <Press>\n";
2015-07-31 22:55:24 -04:00
$bindings = $owner_bindings{$owner};
shift (@$bindings); # remove initial empty element
for my $binding (@$bindings) {
2016-06-22 18:56:35 -04:00
print ' <Binding key="' . @$binding[1] . '" action="' . @$binding[0] . '" group="' . @$binding[2] . "\"/>\n";
2015-07-31 22:55:24 -04:00
}
print " </Press>\n </Bindings>\n";
}
# merge in the "fixed" bindings that are not defined by the argument given to this program
2016-06-22 18:56:35 -04:00
# this covers things like the step editor, monitor and processor box bindings
2015-07-31 22:55:24 -04:00
2021-11-01 14:11:09 -04:00
foreach $hardcoded_bindings ("mixer.bindings", "step_editing.bindings", "monitor.bindings", "processor_box.bindings", "trigger.bindings") {
2015-07-31 22:55:24 -04:00
$path = File::Spec->catfile (dirname ($ARGV[0]), $hardcoded_bindings);
open HARDCODED, "<", $path or die $!;
while (<HARDCODED>) {
print $_;
}
close HARDCODED;
}
2015-08-01 21:43:20 -04:00
print "</BindingSet>\n";
2015-07-11 09:17:59 -04:00
}
2021-03-24 18:33:18 -04:00
if (($make_accelmap || !$make_cheatsheet) && !$html) {
2009-07-31 16:26:15 -04:00
exit 0;
}
2012-12-26 11:44:09 -05:00
if ($html) {
@groups_sorted_by_number = sort { $group_numbering{$a} <=> $group_numbering{$b} } keys %group_numbering;
foreach $gk (@groups_sorted_by_number) {
if ($gk =~ /^m/) {
# mouse stuff - ignore
next;
}
# $bref is a reference to the array of arrays for this group
$bref = $group_bindings{$gk};
if (scalar @$bref > 1) {
$name = $group_names{$gk};
$name =~ s/\\linebreak.*//;
$name =~ s/\\&/&/;
$name =~ s/\$\\_\$/-/g;
$name =~ s/\\[a-z]+ //g;
$name =~ s/[{}]//g;
$name =~ s/\\par//g;
2021-03-24 18:33:18 -04:00
print "<h2>$name</h2>\n";
2012-12-26 11:44:09 -05:00
$gtext = $group_text{$gk};
$gtext =~ s/\\linebreak.*//;
$gtext =~ s/\\&/&/;
$gtext =~ s/\$\\_\$/-/g;
$gtext =~ s/\\[a-z]+ //g;
$gtext =~ s/[{}]//g;
$gtext =~ s/\\par//g;
if (!($gtext eq "")) {
print "$gtext\n\n";
}
# ignore the first entry, which was empty
shift (@$bref);
# set up the list
2021-03-24 18:33:18 -04:00
print "<table class=\"dl\">\n";
2012-12-26 11:44:09 -05:00
# sort the array of arrays by the descriptive text for nicer appearance,
# and print them
for $bbref (sort { @$a[1] cmp @$b[1] } @$bref) {
# $bbref is a reference to an array
$binding = @$bbref[0];
$text = @$bbref[1];
if ($binding =~ /:/) { # mouse binding with "where" clause
($binding,$where) = split (/:/, $binding, 2);
}
foreach $k (keys %cs_modifier_map) {
2021-03-24 18:33:18 -04:00
$binding =~ s/\@$k\@/$html_modifier_map{$k}/;
2012-12-26 11:44:09 -05:00
}
# remove braces for HTML
2021-03-24 18:33:18 -04:00
$binding =~ s/><//g;
2012-12-26 11:44:09 -05:00
$binding =~ s/^<//;
$binding =~ s/>/\+/;
# substitute keycode names for something printable
$re = qr/${ \(join'|', map quotemeta, keys %keycodes)}/;
$binding =~ s/($re)/$keycodes{$1}/g;
# tidy up description
$descr = @$bbref[1];
$descr =~ s/\\linebreak.*//;
$descr =~ s/\\&/&/;
$descr =~ s/\$\\_\$/-/g;
$descr =~ s/\\[a-z]+ //g;
$descr =~ s/[{}]//g;
$descr =~ s/\\par//g;
2021-03-24 18:33:18 -04:00
if ($binding =~ /\+/) {
($mods,$k) = split (/\+/, $binding, 2);
$mods = "mod$mods";
} else {
$mods="";
$k = $binding;
}
print "<tr><th>$descr</th><td><kbd class=\"$mods\">$k</kbd></td></tr>\n";
2012-12-26 11:44:09 -05:00
}
2021-03-24 18:33:18 -04:00
print "</table>\n";
2012-12-26 11:44:09 -05:00
}
}
print " <!-- remove this if more text is added below -->\n";
exit 0;
}
2009-07-30 20:34:36 -04:00
# Now print the cheatsheet
$boilerplate_header = <<END_HEADER;
\\documentclass[10pt,landscape]{article}
2013-12-15 12:31:17 -05:00
%\\documentclass[10pt,landscape,a4paper]{article}
%\\documentclass[10pt,landscape,letterpaper]{article}
2009-07-30 20:34:36 -04:00
\\usepackage{multicol}
\\usepackage{calc}
\\usepackage{ifthen}
\\usepackage{palatino}
2009-07-31 16:26:15 -04:00
\\usepackage{geometry}
2009-07-30 20:34:36 -04:00
2009-07-31 16:26:15 -04:00
\\setlength{\\parskip}{0pt}
\\setlength{\\parsep}{0pt}
\\setlength{\\headsep}{0pt}
\\setlength{\\topskip}{0pt}
\\setlength{\\topmargin}{0pt}
\\setlength{\\topsep}{0pt}
\\setlength{\\partopsep}{0pt}
2009-07-30 20:34:36 -04:00
% This sets page margins to .5 inch if using letter paper, and to 1cm
% if using A4 paper. (This probably isnott strictly necessary.)
% If using another size paper, use default 1cm margins.
\\ifthenelse{\\lengthtest { \\paperwidth = 11in}}
2013-12-15 12:31:17 -05:00
{ \\geometry{top=.5in,left=.5in,right=.5in,bottom=.5in} }
2009-07-30 20:34:36 -04:00
{\\ifthenelse{ \\lengthtest{ \\paperwidth = 297mm}}
{\\geometry{top=1cm,left=1cm,right=1cm,bottom=1cm} }
{\\geometry{top=1cm,left=1cm,right=1cm,bottom=1cm} }
}
% Turn off header and footer
\\pagestyle{empty}
% Redefine section commands to use less space
\\makeatletter
\\renewcommand{\\section}{\\\@startsection{section}{1}{0mm}%
{-1ex plus -.5ex minus -.2ex}%
2009-07-31 16:26:15 -04:00
{0.5ex plus .2ex}%
2009-07-30 20:34:36 -04:00
{\\normalfont\\large\\bfseries}}
\\renewcommand{\\subsection}{\\\@startsection{subsection}{2}{0mm}%
{-1explus -.5ex minus -.2ex}%
{0.5ex plus .2ex}%
{\\normalfont\\normalsize\\bfseries}}
\\renewcommand{\\subsubsection}{\\\@startsection{subsubsection}{3}{0mm}%
{-1ex plus -.5ex minus -.2ex}%
{1ex plus .2ex}%
{\\normalfont\\small\\bfseries}}
\\makeatother
2009-07-31 16:26:15 -04:00
% Do not print section numbers% Do not print section numbers
2009-07-30 20:34:36 -04:00
\\setcounter{secnumdepth}{0}
\\setlength{\\parindent}{0pt}
\\setlength{\\parskip}{0pt plus 0.5ex}
%-------------------------------------------
\\begin{document}
\\newlength{\\MyLen}
\\raggedright
\\footnotesize
\\begin{multicols}{3}
END_HEADER
$boilerplate_footer = <<END_FOOTER;
2009-07-31 16:26:15 -04:00
\\rule{0.3\\linewidth}{0.25pt}
\\scriptsize
2013-12-15 12:31:17 -05:00
Copyright \\copyright\\ 2013 ardour.org
2009-07-31 16:26:15 -04:00
% Should change this to be date of file, not current date.
2013-12-15 12:31:17 -05:00
http://manual.ardour.org
2009-07-31 16:26:15 -04:00
2009-07-30 20:34:36 -04:00
\\end{multicols}
\\end{document}
END_FOOTER
2009-07-31 16:26:15 -04:00
if ($make_cheatsheet) {
print $boilerplate_header;
print "\\begin{center}\\Large\\bf $title \\end{center}\n";
}
2009-07-30 20:34:36 -04:00
@groups_sorted_by_number = sort { $group_numbering{$a} <=> $group_numbering{$b} } keys %group_numbering;
foreach $gk (@groups_sorted_by_number) {
# $bref is a reference to the array of arrays for this group
$bref = $group_bindings{$gk};
if (scalar @$bref > 1) {
2009-07-31 16:26:15 -04:00
print "\\section{$group_names{$gk}}\n";
2009-07-30 20:34:36 -04:00
if (!($group_text{$gk} eq "")) {
print "$group_text{$gk}\n\\par\n";
}
# ignore the first entry, which was empty
shift (@$bref);
# find the longest descriptive text (this is not 100% accuracy due to typography)
$maxtextlen = 0;
$maxtext = "";
for $bbref (@$bref) {
# $bbref is a reference to an array
$text = @$bbref[1];
2009-07-31 16:26:15 -04:00
#
# if there is a linebreak, just use everything up the linebreak
# to determine the width
#
2009-07-30 20:34:36 -04:00
if ($text =~ /\\linebreak/) {
$matchtext = s/\\linebreak.*//;
} else {
$matchtext = $text;
}
if (length ($matchtext) > $maxtextlen) {
$maxtextlen = length ($matchtext);
$maxtext = $matchtext;
}
}
2009-07-31 16:26:15 -04:00
if ($gk =~ /^m/) {
# mouse mode: don't extend max text at all - space it tight
$maxtext .= ".";
} else {
$maxtext .= "....";
}
2009-07-30 20:34:36 -04:00
# set up the table
print "\\settowidth{\\MyLen}{\\texttt{$maxtext}}\n";
print "\\begin{tabular}{\@{}p{\\the\\MyLen}%
\@{}p{\\linewidth-\\the\\MyLen}%
\@{}}\n";
2009-07-31 16:26:15 -04:00
# sort the array of arrays by the descriptive text for nicer appearance,
# and print them
2009-07-30 20:34:36 -04:00
2009-07-31 16:26:15 -04:00
for $bbref (sort { @$a[1] cmp @$b[1] } @$bref) {
2009-07-30 20:34:36 -04:00
# $bbref is a reference to an array
$binding = @$bbref[0];
$text = @$bbref[1];
if ($binding =~ /:/) { # mouse binding with "where" clause
($binding,$where) = split (/:/, $binding, 2);
}
2009-07-31 16:26:15 -04:00
if ($gk =~ /^m/) {
# mouse mode - use shorter abbrevs
foreach $k (keys %mouse_modifier_map) {
$binding =~ s/\@$k\@/$mouse_modifier_map{$k}/;
}
} else {
foreach $k (keys %cs_modifier_map) {
$binding =~ s/\@$k\@/$cs_modifier_map{$k}/;
}
}
2009-07-30 20:34:36 -04:00
$binding =~ s/></\+/g;
$binding =~ s/^<//;
$binding =~ s/>/\+/;
# substitute keycode names for something printable
$re = qr/${ \(join'|', map quotemeta, keys %keycodes)}/;
$binding =~ s/($re)/$keycodes{$1}/g;
# split up mouse bindings to "click" and "where" parts
if ($gk eq "mobject") {
print "{\\tt @$bbref[1] } & {\\tt $binding} {\\it $where}\\\\\n";
} else {
print "{\\tt @$bbref[1] } & {\\tt $binding} \\\\\n";
}
}
print "\\end{tabular}\n";
}
2009-07-30 10:07:49 -04:00
}
2009-07-30 20:34:36 -04:00
print $boilerplate_footer;
2009-07-30 10:07:49 -04:00
exit 0;