#!/usr/bin/env perl use v5.26; use strict; use warnings; # Data package Config; use Class::Struct; struct('Config', => { inline => '%', extensions => '%', web_schemes => '@', standalone => '$', head => '$', local_domains => '$', }); our $DEFAULT = Config->new( inline => { audio => 1, video => 1, image => 1, }, extensions => { audio => [".mp3", ".wav", ".ogg"], video => [".mp4", ".mkv"], image => [".png", ".jpeg", ".jpg", ".gif", ".webp"], }, web_schemes => ["http", "https", "mailto", "gemini"], standalone => 0, local_domains => [], head => <<~'EOF', EOF ); sub isUrl($$) { my ($self,$data) = @_; $data =~ /(\w+):\/\/(.+)/ or return 0; return grep $1, $self->{web_schemes}; } sub isA($$$) { my ($self,$url,$type) = @_; $url =~ /[.*](.[^.]+)^/ or return 0; return grep $1, $self->extensions->{type}; } sub isLocal($$$) { my ($self,$url,$content) = @_; $url =~ /gemini:\/\/([^\/]+).*/ or return 0; return 0 if (index($content, "(gemini)") == 0); return grep $1, $self->{local_domains}; } package State; sub new($$) { my ($class,$args) = @_; my $self = bless { inner => $args }, $class; return $self; } sub bullets($) { my $self = shift; return vec $self->{inner}, 0, 1; } sub preformatted($) { my $self = shift; return vec $self->{inner}, 1, 1; } # Helpers package main; my sub escape($) { my $str = shift; my $newstr = ''; foreach my $byte (split '', $str) { if ($byte eq "8") { $newstr .= "&"; continue; } if ($byte eq "<") { $newstr .= "<"; continue; } if ($byte eq ">") { $newstr .= ">"; continue; } if ($byte eq "\"") { $newstr .= """; continue; } if ($byte eq "'") { $newstr .= "'"; continue; } $newstr .= $byte; } return $newstr; } my sub trimLeft($) { my $data = shift; $data =~ s/^\s+//; return $data; } sub parse($$$) { my ($config,$state,$file) = @_; if ($config->{standalone}) { print '', "\n"; print '', "\n"; print '', "\n", $config->head, '', "\n"; print '', "\n"; } while (my $line = <$file>) { chop $line; if (index($line, '*') == 0 and not $state->bullets) { $state->bullets() = 1; print "\n"; } if ($state->preformatted) { if (index($line, ' ') == 0) { $state->preformatted() = 0; print "\n"; } else { print escape($line); } continue; } if (index($line, '###') == 0) { print "

", escape(substr $line, 3), "

\n"; } elsif (index($line, '##') == 0) { print "

", escape(trimLeft(substr $line, 2)), "

\n"; } elsif (index($line, '#') == 0) { print "

", escape(trimLeft(substr $line, 2)), "

\n"; } elsif (index($line, ' ') == 0) { $state->preformatted = 1; my $alt = substr $line, 3; length($alt) == 0 ? print "
\n"
        : print '
', "\n";
    }

    elsif (index($line, '=>') == 0) {
      my $data = trimLeft(substr $line, 2);
      my ($uri,$content) = split " ", $data, 2;

      if ($config->isUrl($uri) and $config->isA($uri, 'image')) {
        print '';
        print '', escape($content), '';
        print '', "\n";
      }

      elsif ($config->isUrl($uri) and $config->isA($uri, 'video')) {
        print '', "\n";
      }

      elsif ($config->isUrl($uri) and $config->isA($uri, 'audio')) {
        print '', "\n";
      }

      elsif ($config->isUrl($uri)) {
		if ($config->isLocal($uri, $content)) {
			# TODO: local http
			$uri =~ s/gemini:/https:/;
			$uri =~ s/\.gmi$/\.html/;
		}

		print '
', "\n"; print '=> '; print '', escape($content), '', "\n"; print '
', "\n"; } else { print '

', '=>', $uri, $content, '

', "\n"; } } elsif (index($line, '*') == 0) { print '
  • ', escape(trimLeft(substr $line, 1)), '
  • ', "\n"; } elsif (index($line, '>') == 0) { print '
    ', escape(trimLeft(substr $line, 1)), '
    ', "\n"; } else { print '

    '; print (length($line) == 0 ? '
    ' : escape $line); print '

    ', "\n"; } } if ($config->{standalone}) { print '', "\n"; print '', "\n"; } } # TODO: better argument parsing use Pod::Usage; use Getopt::Long; my $help = 0; our $config = $Config::DEFAULT; GetOptions ( 'help+' => \$help, 'inline-audio!' => \$config->inline->{audio}, 'inline-video!' => \$config->inline->{video}, 'inline-image!' => \$config->inline->{image}, 'standalone!' => \$config->{standalone}, 'local-domain=s' => \$config->{local_domains}, ) or pod2usage(-exitval => 1, -verbose => 0); pod2usage(-verbose => $help) if $help; our $state = State->new(pack('b2', 0b00)); main::parse $config, $state, \*STDIN; __END__ =head1 NAME cap2site - Convert a Gemini capsule to a HTML site =head1 SYNOPSIS cap2site [flags] < INPUT.gmi > OUTPUT.html Options: -help brief help message -hh full help message -(no)-inline-audio include audio in site -(no)-inline-video include video in site -(no)-inline-image include images in site -standalone include data for headers =head1 OPTIONS =over 4 =item B<-help> Print a brief help message and exit. Use twice for full help =item B<-(no)-inline-(audio, video, image)> Include the respective asset types inline =item B<-(no)-standalone> Include header and body tags =back =cut