๐Ÿ’พ Archived View for h2903872.stratoserver.net โ€บ GGI โ€บ GGI.pm captured on 2022-03-01 at 15:20:54.

View Raw

More Information

โฌ…๏ธ Previous capture (2021-12-02)

โžก๏ธ Next capture (2022-04-28)

-=-=-=-=-=-=-

package GGI;
#require Exporter;
#@ISA=qw(Exporter);
#@EXPORT=qw();

use strict;
use warnings;
no warnings qw(uninitialized); # avoid warnings for undefined/NULL values!
use utf8::all;
#use URL::Encode;
use Encode;
use URI::Escape;

=encoding utf8

=head1 GGI

GGI.pm - Gemini Gateway Interface: Support perl-programs on Gemini servers.

=head1 SUMMARY

GGI handles the accumulation of multiple parameters in the path-component of the URL through
automatic redirects and offers a simple method to access those parameters from the perl program.

GGI supports the generation of all Gemini line types: responses, headers, text, quotes, links
and pre-formatted, as well as some convenient methods to generate menus and to print tables with
and without ASCII or UniCode frames.

More about the Gemini protocol: L<https://gemini.circumlunar.space/>

=head1 VERSION

GGI.pm Version from 2021-03-26

=head1 AUTHORS

GGI by Frank Jรผdes, L<http://www.linux4specialists.com>

This pod text by Frank Jรผdes, Perl by Larry Wall and the C<perl5-porters>.

=head1 COPYRIGHT

The GGI module is Copyright (c) 2021 Frank Jรผdes, West Virginia, U.S.A.
All rights reserved.

You may distribute under the terms of either the GNU General Public
License or the Artistic License, as specified in the Perl 5.10.0 README file.

=head1 SUPPORT and WARRANTY

The GGI is free Open Source software. IT COMES WITHOUT WARRANTY OF ANY KIND.

=head1 EXAMPLES

=head2 Make the output of the I<ps> utility available through a Gemini server

  #!/bin/perl
  use warnings;
  use strict;
  use utf8::all;
  use GGI;

  my $ggi = new GGI;    # Create a new GGI object and print SUCCESS header
  $ggi->H1('ps test');  # Print a header
  my @output=`ps -fA`;  # Run ps and catch all the output
  chomp @output;        # Remove EOLs
  $ggi->Pre(@output);   # Print the output as preformatted text

=head2 Collect Parameters into the PATH๏ผฟINFO variable

  #!/bin/perl
  use warnings;
  use strict;
  use utf8::all;
  use GGI;

  my $ggi = GGI->new(1);         # New GGI object without writing any header
  if (not defined $ggi->Param) { # No parameter present: Get the amount of parameters wanted
    $ggi->ResponseHeader('INPUT',qq(GGI-Test, ParamTest: How many parameters do you want to test?));
  } # END if

  my $Current = $ggi->Param+1;   # How many parameters are there
  my $Wanted  = $ggi->Param(0);  # How many parameters are wanted?
  if ($Current <= $Wanted) {     # Get the next parameter
    $ggi->ResponseHeader('INPUT',qq(GGI-Test, Paramtest: Please enter a value for Parameter $Current of $Wanted));
  } # END if

  # Done with parameters! - Main program
  $ggi->H1("Hello World!");

  if (defined $ggi->Param) {
    $ggi->H2("The parameters");
    $ggi->List(@{$ggi->Param('*')});
  } else {
    $ggi->H1("No Parameters.");
  } # END if

  $ggi->Link(qq(gemini://h2903872.stratoserver.net/ Go back to the server's home.));

=head2 Implemented methods

The following paragraphs explain the implemented methods of this package in great detail. 

=cut


#----------------------------------------------------------------------------------------------------
=head3 new([NoPrintHeader]

Create a new GGI object:

=over 2

=item * A new object is created, some internal variables initialized.

=item * If there is a parameter at the end of the URL, the value is added as a path-component and
a re-direct to the new URL is sent to the client.

=item * If there is no parameter attached to the URL, all parameters are being copied
from the PATH_INFO into the internal PARAMS array.

=item * If called without any parameters an OK response-header will be printed automatically and the function
will return to the main program. If any TRUE value as parameter is passed, no header will be printed at
all and it is the duty of the calling program to print a Gemini response-header if other than SUCCESS
should be printed.

=back

=cut

sub new {
  my ($Class,$NoPrintHeader) = @_;

  my $self = {};
  bless $self,$Class;

  $self->{HeaderHasBeenSent} = 0;
  $self->{ResponseStatus} = {'INPUT'                       => 10,
                             'SENSITIVE_INPUT'             => 11,
                             'SUCCESS'                     => 20,
                             'REDIRECT_TEMPORARY'          => 30,
                             'REDIRECT_PERMANENT'          => 31,
                             'FAILURE_TEMPORARY'           => 40,
                             'SERVER_UNAVAILABLE'          => 41,
                             'CGI_ERROR'                   => 42,
                             'PROXY_ERROR'                 => 43,
                             'SLOW_DOWN'                   => 44,
                             'FAILURE_PERMANENT'           => 50,
                             'NOT_FOUND'                   => 51,
                             'PROXY_REQUEST_REFUSED'       => 52,
                             'BAD_REQUEST'                 => 53,
                             'CLIENT_CERTIFICATE_REQUIRED' => 60,
                             'CERTIFICATE_NOT_AUTHORIZED'  => 61,
                             'CERTIFICATE_NOT_VALID'       => 62
                             };
                             
  # ********** This is the gemini-server dependend HACKING SECTION **********
  
  # First: Some servers store the PATH_INFO with a leading "/", some other's don't. 
  # So let's remove the leading "/" if it exists:
  if ((length($ENV{PATH_INFO}) > 0) and (substr($ENV{PATH_INFO},0,1) eq '/')) {
    $ENV{PATH_INFO} = substr($ENV{PATH_INFO},1);
  } # END if

  # Second: Some servers store the QUERY_STRING with a leading "?", some other's don't. 
  # So let's remove the leading "?" if it exists:
  if ((length($ENV{QUERY_STRING}) > 0) and (substr($ENV{QUERY_STRING},0,1) eq '?')) {
    $ENV{QUERY_STRING} = substr($ENV{QUERY_STRING},1);
  } # END if

  # Third: Missing CGI variables in the Molly-Brown server!
  # The jetforce-server sets the variables SCRIPT_NAME and GEMINI_URL correctly, the Molly-Brown server does not.
  # This makes the awful hack below necessaryโ€ผ 
  # WARNING: This will only work if the CGI-program is located in /cgi-bin โ€ผ
  # Unless the CGI-variable SCRIPT_NAME is provided by the Molly-Brown-server, this is the only way to 
  # automate the parameter redirection and provide something like the GEMINI_URL
  if ((not defined $ENV{SCRIPT_NAME}) and (defined $ENV{SCRIPT_PATH})) {
    my ($ScriptName) = $ENV{SCRIPT_PATH} =~ m!(/cgi-bin/.*)!;
    $ENV{SCRIPT_NAME} = $ScriptName;
    $ENV{GEMINI_URL} = "gemini://$ENV{SERVER_NAME}${ScriptName}";
    $ENV{GEMINI_URL} .= "/" . $ENV{PATH_INFO} if (length($ENV{PATH_INFO}) > 0);
    $ENV{GEMINI_URL} .= "?" . $ENV{QUERY_STRING} if (length($ENV{QUERY_STRING}) > 0);
  } # END if

  # Handle parameters
  if (length($ENV{QUERY_STRING}) > 0) { 
    # There is a parameter in the URL, so add parameter-value to the path, print redirect header and end the program
    my $URL = $ENV{GEMINI_URL};
    $URL =~ s/\?/\//;
    $self->ResponseHeader('REDIRECT_TEMPORARY',$URL);
    exit 0; # END program and let the clinet execute re-direct.
  } else {  # analyze PATH_INFO and extract parameters
    if (length($ENV{PATH_INFO}) > 0) {
      my $PathInfo = $ENV{PATH_INFO};
#      $PathInfo =~ s/%([0-9,A-F]{2})/'chr(0x'. $1. ')'/eeg;   # URL-Decoding
#      $self->{PARAMS} = [split('/',$PathInfo)];               # split the parameters
#      my $DecodedPathInfo = URL::Encode::url_decode_utf8($PathInfo);
#      $self->{PARAMS} = [split('/',$DecodedPathInfo)];         # split the parameters
      my $DecodedPathInfo = Encode::decode('utf8',uri_unescape($PathInfo));
      $self->{PARAMS} = [split('/',$DecodedPathInfo)];         # split the parameters
    } # END if
  } # END if

  $self->ResponseHeader('SUCCESS') unless (defined $NoPrintHeader); # print an OK header by default
  return $self;
} # END new


#----------------------------------------------------------------------------------------------------
=head3 BaseURL

Returns the current request URL without the parameters
=cut
sub BaseURL {
  return substr($ENV{GEMINI_URL},0,-length($ENV{PATH_INFO}));
} # END BaseURL

#----------------------------------------------------------------------------------------------------
=head3 MyURL

Returns the current request URL - Basically a shortcut for $ENV{GEMINI_URL}.
=cut

sub MyURL {
  return $ENV{GEMINI_URL};
} # END MyURL

#----------------------------------------------------------------------------------------------------
=head3 ServerName

Returns the current request's servername - Basically a shortcut for $ENV{SERVER_NAME}.
=cut

sub ServerName {
  return $ENV{SERVER_NAME};
} # END ServerName

#----------------------------------------------------------------------------------------------------
=head3 ScriptName

Returns the current request's script-name - Basically a shortcut for $ENV{SCRIPT_NAME}.
=cut

sub ScriptName {
  return $ENV{SCRIPT_NAME};
} # END ScriptName

#----------------------------------------------------------------------------------------------------
=head3 WashParameter

Removes potentialy dangerous stuff from the parameters, currently for Linux

=over 2

=item * Everything attached with a ";" at the end, including the ";" is removed

=item * Everything between backticks "`", including the back-ticks themselves, is removed

=item * Everything in '$( )' is removed, including the $-sign and the parentheses

=back

=cut

sub WashParameter {
  my $self = shift @_;
  
  return unless (defined $self->{PARAMS});
  
  map {
    $_ =~ s/;.*$//;       # no command attachment
    $_ =~ s/\`.*\`//;     # no command insertion
    $_ =~ s/\$\(.*?\)//;  # no bash exec
  } @{$self->{PARAMS}};     
} # END WashParameter

#----------------------------------------------------------------------------------------------------
=head3 param([Name])

Access to the GGI-parameters:

=over 2

=item * If there are no GGI parameters present, undef is returned.

=item * Called without an index-value in array-mode, a reference to the parameter-array is returned.

=item * Called without an index-value in scalar-mode the highes index into the parameter array is returned.

=item * Called with the index-value of "#", the numer of parameters is returned.

=item * Called with the index-value of "*", a reference to the parameter-array is returned.

=item * Called with an index-value the value of the indexed parameter is returned if it exists.
If no parameter with the given index exists, I<undef> is returned.

=back

=cut

sub Param {
  my ($self,$Index) = @_;

  return undef unless (defined $self->{PARAMS});
  if (not defined $Index) {
    return $self->{PARAMS} if (wantarray);
    return $#{$self->{PARAMS}};
  } # END if
  return scalar @{$self->{PARAMS}} if ($Index eq '#');
  return $self->{PARAMS} if ($Index eq '*');
  return $self->{PARAMS}->[$Index];
} # END param


#----------------------------------------------------------------------------------------------------
# pr(@Lines) (internal)
#
# Print an array of lines with CR/LF at the end
#----------------------------------------------------------------------------------------------------
sub pr {
  my ($self,@Lines) = @_;
  map { print "$_\r\n"; } @Lines # causing double spacing in some clients
  #map { print "$_\n"; } @Lines
} # END pr

#----------------------------------------------------------------------------------------------------
=head3 print(@Lines)

Print an array of lines with CR/LF at the end
=cut

sub print {
  my ($self,@Lines) = @_;
  $self->ResponseHeader('SUCCESS') unless ($self->{HeaderHasBeenSent}); # send header if necessary
  $self->pr(@Lines);
} # END print

#----------------------------------------------------------------------------------------------------
=head3 ResponseHeader(StatusCode[,Meta])

Print the response header with the meta-information, if present. The following response status-codes
are defined, according to the Gemini protocol documentation:

  - INPUT, SENSITIVE_INPUT
  - SUCCESS
  - REDIRECT_TEMPORARY, REDIRECT_PERMANENT
  - FAILURE_TEMPORARY, SERVER_UNAVAILABLE, CGI_ERROR, PROXY_ERROR, SLOW_DOWN
  - FAILURE_PERMANENT, NOT_FOUND, PROXY_REQUEST_REFUSED, BAD_REQUEST
  - CLIENT_CERTIFICATE_REQUIRED, CERTIFICATE_NOT_AUTHORIZED, CERTIFICATE_NOT_VALID
=cut

sub ResponseHeader {
  my ($self,$StatusCode,$Message) = @_;
  return if ($self->{HeaderHasBeenSent});
  $StatusCode = 'SUCCESS' unless (defined $StatusCode);
  if ($StatusCode eq 'SUCCESS') {
    $self->pr("20 text/gemini");
    $self->{HeaderHasBeenSent} = 1;
  } else {
    if (exists $self->{ResponseStatus}->{$StatusCode}) {
      $self->pr($self->{ResponseStatus}->{$StatusCode} . " $Message");
    } else {
      $self->pr($self->{ResponseStatus}->{CGI_ERROR} . " Unknown Response code '$StatusCode'.");
    } # END if
    exit(0);
  } # END if
} # END ResponseHeader

#----------------------------------------------------------------------------------------------------
=head3 Input(HeaderText)

Print an Input response header with prompt. This is basically a shortcut for I<ResponseHeader('INPUT',[Prompttext]);>
=cut

sub Input {
  my ($self,$Prompt) = @_;
  $self->ResponseHeader('INPUT',$Prompt);
} # END Input

#----------------------------------------------------------------------------------------------------
=head3 H[1-3](HeaderText)

Print headerline 1-3
=cut

sub H1 {
  my ($self,$Line) = @_;
  $self->ResponseHeader('SUCCESS') unless ($self->{HeaderHasBeenSent}); # send header if necessary
  $self->pr("# $Line");
} # END H1

sub H2 {
  my ($self,$Line) = @_;
  $self->ResponseHeader('SUCCESS') unless ($self->{HeaderHasBeenSent}); # send header if necessary
  $self->pr("## $Line");
} # END H2

sub H3 {
  my ($self,$Line) = @_;
  $self->ResponseHeader('SUCCESS') unless ($self->{HeaderHasBeenSent}); # send header if necessary
  $self->pr("### $Line");
} # END H3

#----------------------------------------------------------------------------------------------------
=head3 Pre(@Lines)

Print an array of lines as a preformatted block
=cut

sub Pre {
  my ($self,@Lines) = @_;
  $self->ResponseHeader('SUCCESS') unless ($self->{HeaderHasBeenSent}); # send header if necessary
  $self->pr('```');
  map { print $_,"\n"; } @Lines;
  $self->pr('```');
} # END Pre

#----------------------------------------------------------------------------------------------------


#----------------------------------------------------------------------------------------------------
=head3 LabeledPre(Label,@Lines)

Print an array of lines as a labeled preformatted block
=cut

sub LabeledPre {
  my ($self,$Label,@Lines) = @_;
  $self->ResponseHeader('SUCCESS') unless ($self->{HeaderHasBeenSent}); # send header if necessary
  $self->pr('``` ' . $Label);
  map { print $_,"\n"; } @Lines;
  $self->pr('```');
} # END Pre

#----------------------------------------------------------------------------------------------------

=head3 List(@Lines)

Print an array of lines as an unsorted list.
=cut

sub List {
  my ($self,@Lines) = @_;
  $self->ResponseHeader('SUCCESS') unless ($self->{HeaderHasBeenSent}); # send header if necessary
  map { $self->pr('* ' . $_) } @Lines;
} # END List

#----------------------------------------------------------------------------------------------------
=head3 Link(@Lines)

Print an array of lines as links
=cut

sub Link {
  my ($self,@Lines) = @_;
  $self->ResponseHeader('SUCCESS') unless ($self->{HeaderHasBeenSent}); # send header if necessary
  map { $self->pr('=> ' . $_) } @Lines;
} # END Link

#----------------------------------------------------------------------------------------------------
=head3 Quote(@Lines)

Print an array of lines as quotes
=cut

sub Quote {
  my ($self,@Lines) = @_;
  $self->ResponseHeader('SUCCESS') unless ($self->{HeaderHasBeenSent}); # send header if necessary
  map { $self->pr('> ' . $_) } @Lines;
} # END Link

#----------------------------------------------------------------------------------------------------
=head3 PrintMenu(Menu,BaseURL,Title,Description)

Print the value of I<Title> as an H1-title if defined. Then print
the content of the I<Description> below if defined, followed by the link-menu itself.

I<Menu> is a simple perl-hash with the prompts as keys and the values as content.
These values will be appended to the I<BaseURL> as link-targets and the keys
will become the link description-texts.
=cut

sub PrintMenu {
  my ($self,$Menu,$BaseURL,$Title,$Description) = @_;

  $self->H1($Title) if (defined $Title);
  $self->pr($Description) if (defined $Description);
  my @Links;
  map { push (@Links,"$BaseURL/$Menu{$_} $_") } sort keys %$Menu;
  $self->Link(@Links);
} # END PrintMenu

#----------------------------------------------------------------------------------------------------
=head3 PrintTable(Table,FrameType,Header,Summary,Compact)

Print the content of a two-dimensional table into a pre-formatted block. The I<Table> parameter
is a reference to a two-dimensional array.

It is vital that all cells of the table are defined and that there are no holes in the table structure!

This code will center the column headers and pad all other table-cells to equal length in their column
with the content left aligned. If you want other formatting, you need to pre-format the content of your table.

Valid values for the FrameType are I<ASCII>, I<SINGLE>, I<DOUBLE>, I<SHADOW>, I<FLOAT> and I<NONE>. Invalid 
values will default to I<NONE> and no frames will be printed.

The I<Header> and I<Summary> parameters define weather the first line of the table contains column-headers 
and the last line contains a summary and should be separated from the rest of the table with a horizontal line.

With the I<Compact> parameter the output of horizontal lines between the table data-rows can be supressed. 
(Compact design).
=cut

sub PrintTable {
  my ($self,$Table,$FrameType,$Header,$Summary,$Compact) = @_;

  $FrameType = 'NONE' unless (defined $FrameType);
  $Header    = 'N'    unless (defined $Header);
  $Summary   = 'N'    unless (defined $Summary);
  $Compact   = 'N'    unless (defined $Compact);

  # Determine the table's dimensions
  my $TableRows = @$Table;
  my $LastRow   = ($TableRows-1);
  my $TableCols = @{$Table->[0]};
  my @ColumnWidths;
  for (my $Row=0; $Row < $TableRows; $Row++) {
     for (my $Column=0; $Column < $TableCols; $Column++) {
       if (!(defined $ColumnWidths[$Column]) or ($ColumnWidths[$Column] < length($Table->[$Row]->[$Column]))) {
         $ColumnWidths[$Column] = length($Table->[$Row]->[$Column]);
       } # END if
     } # END $Columns
  } # END for $row
  #DEBUG $self->pr("Rows: $TableRows, Column's: $TableCols");
  #DEBUG $self->pr("Colum-Widths: [" . join(",",@ColumnWidths) . "]");

  # Format all cells to equal column width, center header-columns
  for (my $Row=0; $Row < $TableRows; $Row++) {
     for (my $Column=0; $Column < $TableCols; $Column++) {
       my $CellWidth = length($Table->[$Row]->[$Column]);
       if ($CellWidth < $ColumnWidths[$Column]) {
         if (($Row == 0) and ($Header eq 'Y')) {
           $Table->[$Row]->[$Column] = substr(' ' x (($ColumnWidths[$Column] - $CellWidth)/2) .
                                              $Table->[$Row]->[$Column] .
                                              ' ' x (($ColumnWidths[$Column] - $CellWidth)/2) . '  ',
                                              0,$ColumnWidths[$Column]);
         } else {
           $Table->[$Row]->[$Column] .= ' ' x ($ColumnWidths[$Column] - $CellWidth);
         } # END if
       } # END if
     } # END $Columns
  } # END for $row

  # Create the building blocks for the table frames
  my ($TopLine,$HeaderLine,$HorizontalLine,$SummaryLine,$BottomLine,$Left,$Middle,$Right);
  if ($FrameType eq 'ASCII') {
    ($Left,$Middle,$Right) = ('|','|','|');
    ($TopLine,$HeaderLine,$HorizontalLine,$SummaryLine,$BottomLine) = ('+','+','+','+','+');
    for (my $Column=0; $Column<$TableCols; $Column++) {
       $TopLine        .= '-' x $ColumnWidths[$Column] . '+';
       $HeaderLine     .= '=' x $ColumnWidths[$Column] . '+';
       $HorizontalLine .= '-' x $ColumnWidths[$Column] . '+';
       $SummaryLine    .= '=' x $ColumnWidths[$Column] . '+';
       $BottomLine     .= '-' x $ColumnWidths[$Column] . '+';
    } # END for $Column
  } elsif ($FrameType eq 'SINGLE') {
    ($Left,$Middle,$Right) = ('โ”‚','โ”‚','โ”‚');
    ($TopLine,$HeaderLine,$HorizontalLine,$SummaryLine,$BottomLine) = ('โ•ญ','โ”œ','โ”œ','โ”œ','โ•ฐ');
    for (my $Column=0; $Column<$TableCols; $Column++) {
      if ($Column == ($TableCols-1)) {
        $TopLine        .= 'โ”€' x $ColumnWidths[$Column] . 'โ•ฎ';
        $HeaderLine     .= 'โ”€' x $ColumnWidths[$Column] . 'โ”ค';
        $HorizontalLine .= 'โ”€' x $ColumnWidths[$Column] . 'โ”ค';
        $SummaryLine    .= 'โ”€' x $ColumnWidths[$Column] . 'โ”ค';
        $BottomLine     .= 'โ”€' x $ColumnWidths[$Column] . 'โ•ฏ';
      } else {
        $TopLine        .= 'โ”€' x $ColumnWidths[$Column] . 'โ”ฌ';
        $HeaderLine     .= 'โ”€' x $ColumnWidths[$Column] . 'โ”ผ';
        $HorizontalLine .= 'โ”€' x $ColumnWidths[$Column] . 'โ”ผ';
        $SummaryLine    .= 'โ”€' x $ColumnWidths[$Column] . 'โ”ผ';
        $BottomLine     .= 'โ”€' x $ColumnWidths[$Column] . 'โ”ด';
      } # END if
    } # END for $Column
  } elsif ($FrameType eq 'DOUBLE') {
    ($Left,$Middle,$Right) = ('โ•‘','โ”‚','โ•‘');
    ($TopLine,$HeaderLine,$HorizontalLine,$SummaryLine,$BottomLine) = ('โ•”','โ• ','โ•Ÿ','โ• ','โ•š');
    for (my $Column=0; $Column<$TableCols; $Column++) {
      if ($Column == ($TableCols-1)) {
        $TopLine        .= 'โ•' x $ColumnWidths[$Column] . 'โ•—';
        $HeaderLine     .= 'โ•' x $ColumnWidths[$Column] . 'โ•ฃ';
        $HorizontalLine .= 'โ”€' x $ColumnWidths[$Column] . 'โ•ข';
        $SummaryLine    .= 'โ•' x $ColumnWidths[$Column] . 'โ•ฃ';
        $BottomLine     .= 'โ•' x $ColumnWidths[$Column] . 'โ•';
      } else {
        $TopLine        .= 'โ•' x $ColumnWidths[$Column] . 'โ•ค';
        $HeaderLine     .= 'โ•' x $ColumnWidths[$Column] . 'โ•ช';
        $HorizontalLine .= 'โ”€' x $ColumnWidths[$Column] . 'โ”ผ';
        $SummaryLine    .= 'โ•' x $ColumnWidths[$Column] . 'โ•ช';
        $BottomLine     .= 'โ•' x $ColumnWidths[$Column] . 'โ•ง';
      } # END if
    } # END for $Column
  } elsif ($FrameType eq 'SHADOW') {
    ($Left,$Middle,$Right) = ('โ”‚','โ”‚','โ•‘');
    ($TopLine,$HeaderLine,$HorizontalLine,$SummaryLine,$BottomLine) = ('โ•ญ','โ”œ','โ”œ','โ”œ','โ•˜');
    for (my $Column=0; $Column<$TableCols; $Column++) {
      if ($Column == ($TableCols-1)) {
        $TopLine        .= 'โ”€' x $ColumnWidths[$Column] . 'โ•–';
        $HeaderLine     .= 'โ”€' x $ColumnWidths[$Column] . 'โ•ข';
        $HorizontalLine .= 'โ”€' x $ColumnWidths[$Column] . 'โ•ข';
        $SummaryLine    .= 'โ”€' x $ColumnWidths[$Column] . 'โ•ข';
        $BottomLine     .= 'โ•' x $ColumnWidths[$Column] . 'โ•';
      } else {
        $TopLine        .= 'โ”€' x $ColumnWidths[$Column] . 'โ”ฌ';
        $HeaderLine     .= 'โ”€' x $ColumnWidths[$Column] . 'โ”ผ';
        $HorizontalLine .= 'โ”€' x $ColumnWidths[$Column] . 'โ”ผ';
        $SummaryLine    .= 'โ”€' x $ColumnWidths[$Column] . 'โ”ผ';
        $BottomLine     .= 'โ•' x $ColumnWidths[$Column] . 'โ•ง';
      } # END if
    } # END for $Column
  } elsif ($FrameType eq 'FLOAT') {
    ($Left,$Middle,$Right) = ('โ•‘','โ”‚','โ”‚');
    ($TopLine,$HeaderLine,$HorizontalLine,$SummaryLine,$BottomLine) = ('โ•”','โ•Ÿ','โ•Ÿ','โ•Ÿ','โ•™');
    for (my $Column=0; $Column<$TableCols; $Column++) {
      if ($Column == ($TableCols-1)) {
        $TopLine        .= 'โ•' x $ColumnWidths[$Column] . 'โ••';
        $HeaderLine     .= 'โ”€' x $ColumnWidths[$Column] . 'โ”ค';
        $HorizontalLine .= 'โ”€' x $ColumnWidths[$Column] . 'โ”ค';
        $SummaryLine    .= 'โ”€' x $ColumnWidths[$Column] . 'โ”ค';
        $BottomLine     .= 'โ”€' x $ColumnWidths[$Column] . 'โ•ฏ';
      } else {
        $TopLine        .= 'โ•' x $ColumnWidths[$Column] . 'โ•ค';
        $HeaderLine     .= 'โ”€' x $ColumnWidths[$Column] . 'โ”ผ';
        $HorizontalLine .= 'โ”€' x $ColumnWidths[$Column] . 'โ”ผ';
        $SummaryLine    .= 'โ”€' x $ColumnWidths[$Column] . 'โ”ผ';
        $BottomLine     .= 'โ”€' x $ColumnWidths[$Column] . 'โ”ด';
      } # END if
    } # END for $Column
  } else { # Everything else is equivalent to 'NONE'
    $Left   = ' ';
    $Middle = ' ';
  } # END if

  my @Output;
  push (@Output,$TopLine) if ($FrameType ne 'NONE');
  for (my $Row=0; $Row < $TableRows; $Row++) {
    push (@Output,$HeaderLine) if (($Row == 1) and ($FrameType ne 'NONE') and ($Header eq 'Y'));
    push (@Output,$SummaryLine) if (($Row == $LastRow) and ($FrameType ne 'NONE') and ($Summary eq 'Y'));
    push (@Output,$Left . join ($Middle,@{$Table->[$Row]}) . $Right);
    push (@Output,$HorizontalLine) if (($FrameType ne 'NONE') and ($Compact eq 'N') and ($Row != $LastRow) and
                                       !(($Header eq 'Y') and ($Row == 0)) and
                                       !(($Summary eq 'Y') and ($Row == $LastRow-1)));
  } # END for $row
  push (@Output,$BottomLine) if ($FrameType ne 'NONE');

  $self->Pre(@Output);
} # END PrintTable

#----------------------------------------------------------------------------------------------------
=head3 PrintHash(Hash,Filler,Colon,Quote)

Print the content of a simple perl-hash or a perl-hash of arrays into a pre-formatted block.
The I<Hash> parameter is a reference to a simple pearl-hash or a pearl-has of one-dimensional arrays.
The hash-keys will be printed, filled to equal length with the value of the I<Filler> character,
followed by the value of the I<Colon> parameter, followed by either the hash-value or a list of values
should the content of the hash be an array.
If the I<Quote> parameter is given, values will be encapulated in this quout character.
=cut

sub PrintHash {
  my ($self,$Hash,$Filler,$Colon,$Quote) = @_;

  $Filler = '.' unless (defined $Filler);
  $Colon  = ':' unless (defined $Colon);
  $Quote  = '"' unless (defined $Quote);

  my @Result;
  my $MaxKeyLength = 0;
  map { $MaxKeyLength = length($_) if ($MaxKeyLength < length($_)) } keys %$Hash;
  map {
    if (ref($Hash->{$_}) eq 'ARRAY') {
      push @Result, $_ . ($Filler x ($MaxKeyLength - length($_))) . $Colon . $Quote . join("$Quote, $Quote",@{$Hash->{$_}}) . $Quote;
    } else {
      push @Result, $_ . ($Filler x ($MaxKeyLength - length($_))) . $Colon . $Quote . $Hash->{$_} . $Quote;
    } # END if
  } sort keys %$Hash;
  $self->Pre(@Result);
} # END FormatHash

#----------------------------------------------------------------------------------------------------
=head2 Unicode Trickeries

Unicode contains the code-block `Mathematical Alphanumeric Symbols' [U+1D400..U+1D7FF],
containing styled letters and digits that look like normal characters from the latin alphabet,
just styled in bold, italic, script, outline or gothic.

However those characters will not be recognized as ordinary text by most computer programs,
like search engine crawlers for example.

Note that the digits are only available in bold-style.

B<Warning:> This might not be supported by all Gemini clients! Especially terminal-clients
might not have the necessary Unicode characterset installed, it will definitely screw up the
terminal window of the I<amphora> client on windows.

=head3 UC_Bold(String)

Transform the passed string into pseudo-bold Unicode characters.

=cut

sub UC_Bold {
  my ($self,$Result) = @_;

  $Result =~ tr/abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/๐š๐›๐œ๐๐ž๐Ÿ๐ ๐ก๐ข๐ฃ๐ค๐ฅ๐ฆ๐ง๐จ๐ฉ๐ช๐ซ๐ฌ๐ญ๐ฎ๐ฏ๐ฐ๐ฑ๐ฒ๐ณ๐€๐๐‚๐ƒ๐„๐…๐†๐‡๐ˆ๐‰๐Š๐‹๐Œ๐๐Ž๐๐๐‘๐’๐“๐”๐•๐–๐—๐˜๐™๐ŸŽ๐Ÿ๐Ÿ๐Ÿ‘๐Ÿ’๐Ÿ“๐Ÿ”๐Ÿ•๐Ÿ–๐Ÿ—/;
  return $Result;
} # END UC_Bold

#----------------------------------------------------------------------------------------------------
=head3 UC_Italic(String)

Transform the passed string into pseudo-italic Unicode characters.
=cut

sub UC_Italic {
  my ($self,$Result) = @_;

  $Result =~ tr/abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/๐˜ข๐˜ฃ๐˜ค๐˜ฅ๐˜ฆ๐˜ง๐˜จ๐˜ฉ๐˜ช๐˜ซ๐˜ฌ๐˜ญ๐˜ฎ๐˜ฏ๐˜ฐ๐˜ฑ๐˜ฒ๐˜ณ๐˜ด๐˜ต๐˜ถ๐˜ท๐˜ธ๐˜น๐˜บ๐˜ป๐˜ˆ๐˜‰๐˜Š๐˜‹๐˜Œ๐˜๐˜Ž๐˜๐˜๐˜‘๐˜’๐˜“๐˜”๐˜•๐˜–๐˜—๐˜˜๐˜™๐˜š๐˜›๐˜œ๐˜๐˜ž๐˜Ÿ๐˜ ๐˜ก/;
  return $Result;
} # END UC_Italic

#----------------------------------------------------------------------------------------------------
=head3 UC_BoldItalic(String)

Transform the passed string into pseudo-bold-italic Unicode characters.
=cut

sub UC_BoldItalic {
  my ($self,$Result) = @_;

  $Result =~ tr/abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/๐™–๐™—๐™˜๐™™๐™š๐™›๐™œ๐™๐™ž๐™Ÿ๐™ ๐™ก๐™ข๐™ฃ๐™ค๐™ฅ๐™ฆ๐™ง๐™จ๐™ฉ๐™ช๐™ซ๐™ฌ๐™ญ๐™ฎ๐™ฏ๐˜ผ๐˜ฝ๐˜พ๐˜ฟ๐™€๐™๐™‚๐™ƒ๐™„๐™…๐™†๐™‡๐™ˆ๐™‰๐™Š๐™‹๐™Œ๐™๐™Ž๐™๐™๐™‘๐™’๐™“๐™”๐™•/;
  return $Result;
} # END UC_BoldItalic

#----------------------------------------------------------------------------------------------------
=head3 UC_Script(String)

Transform the passed string into pseudo-script Unicode characters.
=cut

sub UC_Script {
  my ($self,$Result) = @_;

  $Result =~ tr/abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/๐’ถ๐’ท๐’ธ๐’นโ„ฏ๐’ปโ„Š๐’ฝ๐’พ๐’ฟ๐“€๐“๐“‚๐“ƒโ„ด๐“…๐“†๐“‡๐“ˆ๐“‰๐“Š๐“‹๐“Œ๐“๐“Ž๐“๐’œโ„ฌ๐’ž๐’Ÿโ„ฐโ„ฑ๐’ขโ„‹โ„๐’ฅ๐’ฆโ„’โ„ณ๐’ฉ๐’ช๐’ซ๐’ฌโ„›๐’ฎ๐’ฏ๐’ฐ๐’ฑ๐’ฒ๐’ณ๐’ด๐’ต/;
  return $Result;
} # END UC_Script

#----------------------------------------------------------------------------------------------------
=head3 UC_BoldScript(String)

Transform the passed string into pseudo-bold-script Unicode characters.
=cut

sub UC_BoldScript {
  my ($self,$Result) = @_;

  $Result =~ tr/abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/๐“ช๐“ซ๐“ฌ๐“ญ๐“ฎ๐“ฏ๐“ฐ๐“ฑ๐“ฒ๐“ณ๐“ด๐“ต๐“ถ๐“ท๐“ธ๐“น๐“บ๐“ป๐“ผ๐“ฝ๐“พ๐“ฟ๐”€๐”๐”‚๐”ƒ๐“๐“‘๐“’๐““๐“”๐“•๐“–๐“—๐“˜๐“™๐“š๐“›๐“œ๐“๐“ž๐“Ÿ๐“ ๐“ก๐“ข๐“ฃ๐“ค๐“ฅ๐“ฆ๐“ง๐“จ๐“ฉ/;
  return $Result;
} # END UC_BoldScript

#----------------------------------------------------------------------------------------------------
=head3 UC_Outline(String)

Transform the passed string into pseudo-outline Unicode characters.
=cut

sub UC_Outline {
  my ($self,$Result) = @_;

  $Result =~ tr/abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/๐•’๐•“๐•”๐••๐•–๐•—๐•˜๐•™๐•š๐•›๐•œ๐•๐•ž๐•Ÿ๐• ๐•ก๐•ข๐•ฃ๐•ค๐•ฅ๐•ฆ๐•ง๐•จ๐•ฉ๐•ช๐•ซ๐”ธ๐”นโ„‚๐”ป๐”ผ๐”ฝ๐”พโ„๐•€๐•๐•‚๐•ƒ๐•„โ„•๐•†โ„™โ„šโ„๐•Š๐•‹๐•Œ๐•๐•Ž๐•๐•โ„ค๐Ÿ˜๐Ÿ™๐Ÿš๐Ÿ›๐Ÿœ๐Ÿ๐Ÿž๐ŸŸ๐Ÿ ๐Ÿก/;
  return $Result;
} # END UC_Outline

#----------------------------------------------------------------------------------------------------
=head3 UC_Gothic(String)

Transform the passed string into pseudo-gothic Unicode characters.
=cut

sub UC_Gothic {
  my ($self,$Result) = @_;

  $Result =~ tr/abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/๐”ž๐”Ÿ๐” ๐”ก๐”ข๐”ฃ๐”ค๐”ฅ๐”ฆ๐”ง๐”จ๐”ฉ๐”ช๐”ซ๐”ฌ๐”ญ๐”ฎ๐”ฏ๐”ฐ๐”ฑ๐”ฒ๐”ณ๐”ด๐”ต๐”ถ๐”ท๐”„๐”…โ„ญ๐”‡๐”ˆ๐”‰๐”Šโ„Œโ„‘๐”๐”Ž๐”๐”๐”‘๐”’๐”“๐””โ„œ๐”–๐”—๐”˜๐”™๐”š๐”›๐”œ/;
  return $Result;
} # END UC_Gothic

#----------------------------------------------------------------------------------------------------
=head3 UC_BoldGothic(String)

Transform the passed string into pseudo-bold-gothic Unicode characters.
=cut

sub UC_BoldGothic {
  my ($self,$Result) = @_;

  $Result =~ tr/abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/๐–†๐–‡๐–ˆ๐–‰๐–Š๐–‹๐–Œ๐–๐–Ž๐–๐–๐–‘๐–’๐–“๐–”๐–•๐––๐–—๐–˜๐–™๐–š๐–›๐–œ๐–๐–ž๐–Ÿ๐•ฌ๐•ญ๐•ฎ๐•ฏ๐•ฐ๐•ฑ๐•ฒ๐•ณ๐•ด๐•ต๐•ถ๐•ท๐•ธ๐•น๐•บ๐•ป๐•ผ๐•ฝ๐•พ๐•ฟ๐–€๐–๐–‚๐–ƒ๐–„๐–…/;
  return $Result;
} # END UC_BoldGothic

# ---------------------------------------------------------------------------------
# End of package
1;