123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559 |
- #!/usr/bin/perl
- =encoding utf8
- =head1 ABOUT
- A basic website assembler, written in Perl 5.
- © 2019 Tirifto <tirifto@posteo.cz>
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
- You should have received a copy of the GNU General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
- =for REUSE
- SPDX-Licence-Identifier: GPL-3.0-or-later
- =cut
- use v5.22;
- use feature 'postderef'; # Postfix dereferencing.
- use strict;
- use warnings;
- =for comment
- Below lie the sealed artifacts of UNICODE handling. They were brought in for
- blind adherence to noble ideals of character encoding, which turned out to be a
- mistake, for many functions of the program were left broken. It would seem one
- shouldn't dabble in arcane arts they don't understand well, and leave the magic
- of Perl do its tricks. Perhaps one day we shall grow wise, and peel off the seal
- once more, to better bend bytes to our will.
- =cut
- # use utf8;
- # binmode STDIN, ':utf8';
- # binmode STDOUT, ':utf8';
- # binmode STDERR, ':utf8';
- use File::Find; # Traverse directory tree.
- use Getopt::Long
- qw(:config no_auto_abbrev bundling); # Command line parameters.
- use List::Util
- qw(any); # Find item in array.
- use Path::Tiny; # Object-oriented handling of files.
- my $version = "too low"; # Far too early in development.
- my $help = <<'END';
- Usage: ./pageling [OPTIONS]
- Options:
- --config, -c [DIRECTORY] Set directory with configuration files.
- --help, -h Print this help.
- --input, -i [DIRECTORY] Set input directory.
- --output, -o [DIRECTORY] Set output directory.
- --verbose, -v Be more verbose in output.
- ×1: Verbose.
- ×2: Very verbose.
- ×3: Very very verbose.
- ×4: Debug.
- --version, Print script name and version.
- Report bugs to <tirifto@posteo.cz>.
- END
- =head1 INITIALISE
- =head2 Variables
- =head3 Directories
- =over
- =item
- C<$dRootIn> has the root input directory
- =item
- C<$dRootOut> has the root output directory
- =back
- =cut
- my ($flagHelp, $verbose, $flagVersion) = (0, 0, 0);
- my ($dRootIn, $dRootOut, $dConf) = ('', '', '');
- GetOptions ("config|c" => \$dConf,
- "help|h" => \$flagHelp,
- "input|i=s" => \$dRootIn,
- "output|o=s" => \$dRootOut,
- "verbose|v+" => \$verbose,
- "version" => \$flagVersion)
- or die "Error reading options.\n";
- if ($flagHelp) {
- say $help;
- exit;
- } elsif ($flagVersion) {
- say "Pageling, version ‘$version’";
- exit;
- }
- -e $dRootIn
- or die "Input directory ‘$dRootIn’ does not exist";
- -e $dRootOut or mkdir $dRootOut
- or die "Couldn't create output directory ‘$dRootOut’";
- =head2 Classes
- =head3 Pali
- Abstract class from which the more specific L</Pali::D> and L</Pali::F> classes
- are derived. The objects are implemented as hash tables. They evaluate to their
- paths in string context.
- =head4 Class attributes
- =over
- =item %registrations
- Absolute (and clean) paths hashed to Pali::F and Pali::D objects. Used to look
- up objects in order to preserve order by avoiding the existence of multiple
- objects for a single file/directory.
- =back
- =head4 Object attributes
- =over
- =item $path
- Contains the Path::Tiny object representing the path to the file/directory.
- =back
- =head4 Methods
- =over
- =item new
- $path (+) → $object
- Takes a path to an existing file/directory as a list of arguments, which will
- then be handed Path::Tiny to make a path object (so refer to that to see what
- forms are permitted). Returns an object of the given class.
- =item setPath
- $path (+) →|
- Makes a new path for the object (via Path::Tiny) from the list of
- arguments. Consider not using it if you don't move input files around.
- =item getPath
- |→ $string
- Returns the path to the file/directory in string form.
- =item setFOut
- $language, $path (+) →|
- Sets the ouptut file for the given language code to the given path (via
- Path::Tiny).
- =item getFout
- $language → $path
- Returns the path to the output file for the given language code.
- =item setTit
- $language, $string (+) →|
- Sets the title for the given language code to the given string. Multiple strings
- will eventually be joined together with spaces in between.
- =item getTit
- $language → $string
- Returns the title for the given language code.
- =item addLan
- $language →|
- Adds the given language code to the file/directory's list of used language
- codes. You might want to use this when entering a new language section.
- =item getLans
- |→ @languages
- Returns an array of language codes associated with the file/directory.
- =item parent
- |→ $object
- Returns an object of the file/directory's parent directory.
- =item grandparent
- |→ $object
- Returns an object of the file/directory's parent's parent directory. Remember to
- visit it every once in a while!
- =back
- =cut
- {
- package Pali;
- use parent qw(Path::Tiny);
- my %registrations;
- # Make the object stringify to its path.
- use overload (
- q("") => 'getPath'
- );
-
- sub new {
- my ($class, @args) = @_;
- my $self = {path => (Path::Tiny::path @args)->realpath};
- bless $self, $class;
- say "‣‣‣ Making new file of $self!" if $verbose >= 4;
- if (exists $registrations{$self->getPath}) {
- return $registrations{$self->getPath};
- } else {
- return $registrations{$self->getPath} = $self;
- }
- }
- sub setPath {
- my ($self, @args) = @_;
- $self->{path} = (Path::Tiny::path @args)->realpath;
- }
-
- sub getPath {
- my $self = shift;
- return ($self->{path})->stringify;
- }
- sub setFOut {
- my ($self, $lan, @args) = @_;
- $self->{lans}{$lan}{fOut} = (Path::Tiny::path @args)->realpath;
- }
- sub getFOut {
- my ($self, $lan) = @_;
- return $self->{lans}{$lan}{fOut};
- }
- sub setTit {
- my ($self, $lan, @args) = @_;
- $self->{lans}{$lan}{tit} = join " ", @args;
- }
- sub getTit {
- my ($self, $lan) = @_;
- return $self->{lans}{$lan}{tit};
- }
- sub addLan {
- my ($self, $lan) = @_;
- $self->{lans}{$lan} = {};
- }
- sub getLans {
- my $self = shift;
- my @languages = keys $self->{lans}->%*;
- keys $self->{lans}->%*; # Reset iterator.
- return @languages;
- }
- sub parent {
- my $self = shift;
- return Pali::D->new (($self->{path})->parent);
- }
- sub grandparent {
- my $self = shift;
- my $parentPath = ($self->{path})->parent;
- return Pali::D->new ($parentPath->parent);
- }
- }
- =head3 Pali::D
- Pageling directory. Subclass of L</Pali>. Preferably use it for input
- directories which have been breathed into existence.
- =head4 Methods
- =over
- =item addNav
- $object →|
- Takes a Pali file/directory object and adds it to the directory's navigation.
- =item getNavs
- |→ @navigation
- Returns an array of Pali file/directory objects holding the directory's
- navigation.
- =back
- =cut
- {
- package Pali::D;
- our @ISA = qw(Pali);
- sub addNav {
- my ($self, $nav) = @_;
- push $self->{navs}->@*, $nav;
- }
- sub getNavs {
- my $self = shift;
- return $self->{navs}->@*;
- }
- }
- =head3 Pali::F
- Pageling file. Subclass of L</Pali>. Preferably use it for input files which
- have been breathed into existence.
- =head4 Methods
- =over
- =item markIndex
- |→|
- Makes the file remember that it is an index file to a directory.
- =item isIndex
- |→ $boolean
- Returns true if the file recalls being an index file to a directory. And, as you
- may have guessed, returns false if not.
- =back
- =cut
- {
- package Pali::F;
- our @ISA = qw(Pali);
- sub markIndex {
- my $self = shift;
- $self->{index} = 1;
- }
- sub isIndex {
- my $self = shift;
- return ($self->{index} ? 1 : 0);
- }
- }
- =head2 Subroutines
- =head3 C<readPaliya>
- [$CONF FILE] → [\%KEYWORDS ⇒ VALUES]
- Takes a Paliya configuration file, parses it, and returns a reference
- to all the keywords mapped to values. Dies on wrong formatting.
- =cut
- sub readPaliya {
- my $fIn = shift @_;
- my $fhIn;
- open ($fhIn, '<', $fIn) or die "Couldn't open ‘$fIn’";
- my %conf;
- my ($key, $value) = ('', '');
- KEY0: while (read ($fhIn, $_, 1) != 0) {
- next KEY0 if /\s/; # W
- if (/(\{|\})/) { # { }
- die "Keyword can't contain braces in ‘$fIn’";
- } else { # C \
- $key .= $_;
- KEY1: while (read ($fhIn, $_, 1) != 0) {
- if (/(\s|\})/) { # W }
- die "Wrong character in keyword in ‘$fIn’ after ‘$key’";
- } elsif (/\{/) { # {
- VALUE: while (read ($fhIn, $_, 1) != 0) {
- if (/\\/) { # \
- read ($fhIn, $_, 1) or last KEY0;
- $value .= $_;
- } elsif (/\}/) { # }
- $conf{$key} = $value;
- ($key, $value) = ('', '');
- next KEY0;
- } else { # C W {
- $value .= $_;
- next VALUE;
- }
- }
- } else { # C \
- $key .= $_;
- next KEY1;
- }
- }
- }
- }
- return \%conf;
- }
- =head2 Input and output directories
- Paths to input and output directories are turned into L</Pali::Dir>
- objects. Because all other paths are practically going to be derived from these,
- it is necessary that we resolve them to remove any relative jumps (‘.’ and
- ‘..’), lest our chances of success perish in a chaotic maze of indirection.
- =cut
- $dRootIn = Pali::D->new ($dRootIn);
- $dRootOut = Pali::D->new ($dRootOut);
- $dConf = ($dConf
- ? Pali::D->new ($dConf)
- : Pali::D->new ($dRootIn->getPath, "pageling"));
- =head2 Configuration files
- Content of the configuration files is read into three variables:
- C<%languages>, C<%navigation>, and C<%switcher>.
- =cut
- my %fConf = (
- 'languages' => path ($dConf->getPath, "languages.paliya"),
- 'navigation' => path ($dConf->getPath, "navigation.paliya"),
- 'switcher' => path ($dConf->getPath, "switcher.paliya"),
- );
- -e $fConf{languages} or die "Missing language configuration file"
- . " ‘$fConf{languages}’";
- -e $fConf{navigation} or die "Missing navigation configuration file"
- . " ‘$fConf{navigation}’";
- -e $fConf{switcher} or die "Missing switcher configuration file"
- . " ‘$fConf{switcher}’";
- my %languages = %{readPaliya $fConf{languages}};
- my %navigation = %{readPaliya $fConf{navigation}};
- my %switcher = %{readPaliya $fConf{switcher}};
- my @fIn;
- find (\®isterFiles, $dRootIn->getPath);
- sub registerFiles {
- say "‣‣‣ Examining $_" if $verbose >= 4;
- if (-d || /(.paliya|\#|\~)$/) {
- say "‣‣‣ Not delving into that!" if $verbose >= 4;
- return;
- } else {
- my $f = Pali::F->new ($_);
- $f->markIndex if $f =~ /^index\./;
- push @fIn, $f;
- say "‣‣‣ Got it!" if $verbose >= 4;
- }
- }
- foreach my $lang (keys %languages) {
- $dRootIn->setFOut ($lang, $dRootOut);
- }
- keys %languages; # Reset iterator.
- say "Processing input files…" if $verbose >=1;
- # find (\&firstPass, $dRootIn);
- foreach my $fIn (@fIn) {
- say "• File: $fIn" if $verbose >= 2;
- my ($lan, $fil, $tit, $nav) = ('', '', '', ''); # Registered directives
- my ($fOut, $fhIn, $fhOut);
- open $fhIn, '<', $fIn->getPath;
- while (my $line = <$fhIn>) {
- if ($line =~ s/^:::\s*//) { # This is a directive!
- my ($key, $value) = each %{readPaliya (\$line)};
- if ($key =~ /^(lan|language|lin|lingvo)$/) {
- ($lan, $fil, $tit) = ($value, '', '');
- $fIn->addLan ($lan);
- ($fIn->parent)->addLan ($lan) if $fIn->isIndex;
- say " ◦ Processing language ‘$lan’" if $verbose >= 3;
- } elsif ($key =~ /^(fil|file|dos|dosiero)$/) {
- $fil = $value if $lan;
- my $parent = $fIn->parent;
- if ($fIn->isIndex) {
- my $grandparent = $parent->parent;
- my $fParentOut = path ($grandparent->getFOut ($lan), $fil);
- $parent->setFOut ($lan, $fParentOut);
- $fOut = path ($parent->getFOut ($lan), "index.html");
- } else {
- $fOut = path ($parent->getFOut ($lan), $fil);
- }
- $fIn->setFOut ($lan, $fOut);
- open $fhOut, '>', $fOut or die "Couldn't open ‘$fOut’";
- say " ◦ Output file set to ‘$fOut’" if $verbose >= 3;
- } elsif ($key =~ /^(tit|title|titolo)$/) {
- $tit = $value if $lan;
- $fIn->setTit ($lan, $tit);
- say " ◦ Title set to ‘$tit’" if $verbose >= 3;
- } elsif ($key =~ /^(nav|navigation|navigilo)$/) {
- $nav = 'yes';
- ($fIn->parent)->addNav ($fIn);
- say " ◦ Added to parent's navigation" if $verbose >= 3;
- } else {
- say "Unrecognised directive found in ‘$fIn’!";
- }
- next;
- } else {
- if ($fil) {
- print $fhOut $line;
- }
- }
- }
- }
|