pageling.pl 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559
  1. #!/usr/bin/perl
  2. =encoding utf8
  3. =head1 ABOUT
  4. A basic website assembler, written in Perl 5.
  5. © 2019 Tirifto <tirifto@posteo.cz>
  6. This program is free software: you can redistribute it and/or modify
  7. it under the terms of the GNU General Public License as published by
  8. the Free Software Foundation, either version 3 of the License, or
  9. (at your option) any later version.
  10. This program is distributed in the hope that it will be useful,
  11. but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. GNU General Public License for more details.
  14. You should have received a copy of the GNU General Public License
  15. along with this program. If not, see <https://www.gnu.org/licenses/>.
  16. =for REUSE
  17. SPDX-Licence-Identifier: GPL-3.0-or-later
  18. =cut
  19. use v5.22;
  20. use feature 'postderef'; # Postfix dereferencing.
  21. use strict;
  22. use warnings;
  23. =for comment
  24. Below lie the sealed artifacts of UNICODE handling. They were brought in for
  25. blind adherence to noble ideals of character encoding, which turned out to be a
  26. mistake, for many functions of the program were left broken. It would seem one
  27. shouldn't dabble in arcane arts they don't understand well, and leave the magic
  28. of Perl do its tricks. Perhaps one day we shall grow wise, and peel off the seal
  29. once more, to better bend bytes to our will.
  30. =cut
  31. # use utf8;
  32. # binmode STDIN, ':utf8';
  33. # binmode STDOUT, ':utf8';
  34. # binmode STDERR, ':utf8';
  35. use File::Find; # Traverse directory tree.
  36. use Getopt::Long
  37. qw(:config no_auto_abbrev bundling); # Command line parameters.
  38. use List::Util
  39. qw(any); # Find item in array.
  40. use Path::Tiny; # Object-oriented handling of files.
  41. my $version = "too low"; # Far too early in development.
  42. my $help = <<'END';
  43. Usage: ./pageling [OPTIONS]
  44. Options:
  45. --config, -c [DIRECTORY] Set directory with configuration files.
  46. --help, -h Print this help.
  47. --input, -i [DIRECTORY] Set input directory.
  48. --output, -o [DIRECTORY] Set output directory.
  49. --verbose, -v Be more verbose in output.
  50. ×1: Verbose.
  51. ×2: Very verbose.
  52. ×3: Very very verbose.
  53. ×4: Debug.
  54. --version, Print script name and version.
  55. Report bugs to <tirifto@posteo.cz>.
  56. END
  57. =head1 INITIALISE
  58. =head2 Variables
  59. =head3 Directories
  60. =over
  61. =item
  62. C<$dRootIn> has the root input directory
  63. =item
  64. C<$dRootOut> has the root output directory
  65. =back
  66. =cut
  67. my ($flagHelp, $verbose, $flagVersion) = (0, 0, 0);
  68. my ($dRootIn, $dRootOut, $dConf) = ('', '', '');
  69. GetOptions ("config|c" => \$dConf,
  70. "help|h" => \$flagHelp,
  71. "input|i=s" => \$dRootIn,
  72. "output|o=s" => \$dRootOut,
  73. "verbose|v+" => \$verbose,
  74. "version" => \$flagVersion)
  75. or die "Error reading options.\n";
  76. if ($flagHelp) {
  77. say $help;
  78. exit;
  79. } elsif ($flagVersion) {
  80. say "Pageling, version ‘$version’";
  81. exit;
  82. }
  83. -e $dRootIn
  84. or die "Input directory ‘$dRootIn’ does not exist";
  85. -e $dRootOut or mkdir $dRootOut
  86. or die "Couldn't create output directory ‘$dRootOut’";
  87. =head2 Classes
  88. =head3 Pali
  89. Abstract class from which the more specific L</Pali::D> and L</Pali::F> classes
  90. are derived. The objects are implemented as hash tables. They evaluate to their
  91. paths in string context.
  92. =head4 Class attributes
  93. =over
  94. =item %registrations
  95. Absolute (and clean) paths hashed to Pali::F and Pali::D objects. Used to look
  96. up objects in order to preserve order by avoiding the existence of multiple
  97. objects for a single file/directory.
  98. =back
  99. =head4 Object attributes
  100. =over
  101. =item $path
  102. Contains the Path::Tiny object representing the path to the file/directory.
  103. =back
  104. =head4 Methods
  105. =over
  106. =item new
  107. $path (+) → $object
  108. Takes a path to an existing file/directory as a list of arguments, which will
  109. then be handed Path::Tiny to make a path object (so refer to that to see what
  110. forms are permitted). Returns an object of the given class.
  111. =item setPath
  112. $path (+) →|
  113. Makes a new path for the object (via Path::Tiny) from the list of
  114. arguments. Consider not using it if you don't move input files around.
  115. =item getPath
  116. |→ $string
  117. Returns the path to the file/directory in string form.
  118. =item setFOut
  119. $language, $path (+) →|
  120. Sets the ouptut file for the given language code to the given path (via
  121. Path::Tiny).
  122. =item getFout
  123. $language → $path
  124. Returns the path to the output file for the given language code.
  125. =item setTit
  126. $language, $string (+) →|
  127. Sets the title for the given language code to the given string. Multiple strings
  128. will eventually be joined together with spaces in between.
  129. =item getTit
  130. $language → $string
  131. Returns the title for the given language code.
  132. =item addLan
  133. $language →|
  134. Adds the given language code to the file/directory's list of used language
  135. codes. You might want to use this when entering a new language section.
  136. =item getLans
  137. |→ @languages
  138. Returns an array of language codes associated with the file/directory.
  139. =item parent
  140. |→ $object
  141. Returns an object of the file/directory's parent directory.
  142. =item grandparent
  143. |→ $object
  144. Returns an object of the file/directory's parent's parent directory. Remember to
  145. visit it every once in a while!
  146. =back
  147. =cut
  148. {
  149. package Pali;
  150. use parent qw(Path::Tiny);
  151. my %registrations;
  152. # Make the object stringify to its path.
  153. use overload (
  154. q("") => 'getPath'
  155. );
  156. sub new {
  157. my ($class, @args) = @_;
  158. my $self = {path => (Path::Tiny::path @args)->realpath};
  159. bless $self, $class;
  160. say "‣‣‣ Making new file of $self!" if $verbose >= 4;
  161. if (exists $registrations{$self->getPath}) {
  162. return $registrations{$self->getPath};
  163. } else {
  164. return $registrations{$self->getPath} = $self;
  165. }
  166. }
  167. sub setPath {
  168. my ($self, @args) = @_;
  169. $self->{path} = (Path::Tiny::path @args)->realpath;
  170. }
  171. sub getPath {
  172. my $self = shift;
  173. return ($self->{path})->stringify;
  174. }
  175. sub setFOut {
  176. my ($self, $lan, @args) = @_;
  177. $self->{lans}{$lan}{fOut} = (Path::Tiny::path @args)->realpath;
  178. }
  179. sub getFOut {
  180. my ($self, $lan) = @_;
  181. return $self->{lans}{$lan}{fOut};
  182. }
  183. sub setTit {
  184. my ($self, $lan, @args) = @_;
  185. $self->{lans}{$lan}{tit} = join " ", @args;
  186. }
  187. sub getTit {
  188. my ($self, $lan) = @_;
  189. return $self->{lans}{$lan}{tit};
  190. }
  191. sub addLan {
  192. my ($self, $lan) = @_;
  193. $self->{lans}{$lan} = {};
  194. }
  195. sub getLans {
  196. my $self = shift;
  197. my @languages = keys $self->{lans}->%*;
  198. keys $self->{lans}->%*; # Reset iterator.
  199. return @languages;
  200. }
  201. sub parent {
  202. my $self = shift;
  203. return Pali::D->new (($self->{path})->parent);
  204. }
  205. sub grandparent {
  206. my $self = shift;
  207. my $parentPath = ($self->{path})->parent;
  208. return Pali::D->new ($parentPath->parent);
  209. }
  210. }
  211. =head3 Pali::D
  212. Pageling directory. Subclass of L</Pali>. Preferably use it for input
  213. directories which have been breathed into existence.
  214. =head4 Methods
  215. =over
  216. =item addNav
  217. $object →|
  218. Takes a Pali file/directory object and adds it to the directory's navigation.
  219. =item getNavs
  220. |→ @navigation
  221. Returns an array of Pali file/directory objects holding the directory's
  222. navigation.
  223. =back
  224. =cut
  225. {
  226. package Pali::D;
  227. our @ISA = qw(Pali);
  228. sub addNav {
  229. my ($self, $nav) = @_;
  230. push $self->{navs}->@*, $nav;
  231. }
  232. sub getNavs {
  233. my $self = shift;
  234. return $self->{navs}->@*;
  235. }
  236. }
  237. =head3 Pali::F
  238. Pageling file. Subclass of L</Pali>. Preferably use it for input files which
  239. have been breathed into existence.
  240. =head4 Methods
  241. =over
  242. =item markIndex
  243. |→|
  244. Makes the file remember that it is an index file to a directory.
  245. =item isIndex
  246. |→ $boolean
  247. Returns true if the file recalls being an index file to a directory. And, as you
  248. may have guessed, returns false if not.
  249. =back
  250. =cut
  251. {
  252. package Pali::F;
  253. our @ISA = qw(Pali);
  254. sub markIndex {
  255. my $self = shift;
  256. $self->{index} = 1;
  257. }
  258. sub isIndex {
  259. my $self = shift;
  260. return ($self->{index} ? 1 : 0);
  261. }
  262. }
  263. =head2 Subroutines
  264. =head3 C<readPaliya>
  265. [$CONF FILE] → [\%KEYWORDS ⇒ VALUES]
  266. Takes a Paliya configuration file, parses it, and returns a reference
  267. to all the keywords mapped to values. Dies on wrong formatting.
  268. =cut
  269. sub readPaliya {
  270. my $fIn = shift @_;
  271. my $fhIn;
  272. open ($fhIn, '<', $fIn) or die "Couldn't open ‘$fIn’";
  273. my %conf;
  274. my ($key, $value) = ('', '');
  275. KEY0: while (read ($fhIn, $_, 1) != 0) {
  276. next KEY0 if /\s/; # W
  277. if (/(\{|\})/) { # { }
  278. die "Keyword can't contain braces in ‘$fIn’";
  279. } else { # C \
  280. $key .= $_;
  281. KEY1: while (read ($fhIn, $_, 1) != 0) {
  282. if (/(\s|\})/) { # W }
  283. die "Wrong character in keyword in ‘$fIn’ after ‘$key’";
  284. } elsif (/\{/) { # {
  285. VALUE: while (read ($fhIn, $_, 1) != 0) {
  286. if (/\\/) { # \
  287. read ($fhIn, $_, 1) or last KEY0;
  288. $value .= $_;
  289. } elsif (/\}/) { # }
  290. $conf{$key} = $value;
  291. ($key, $value) = ('', '');
  292. next KEY0;
  293. } else { # C W {
  294. $value .= $_;
  295. next VALUE;
  296. }
  297. }
  298. } else { # C \
  299. $key .= $_;
  300. next KEY1;
  301. }
  302. }
  303. }
  304. }
  305. return \%conf;
  306. }
  307. =head2 Input and output directories
  308. Paths to input and output directories are turned into L</Pali::Dir>
  309. objects. Because all other paths are practically going to be derived from these,
  310. it is necessary that we resolve them to remove any relative jumps (‘.’ and
  311. ‘..’), lest our chances of success perish in a chaotic maze of indirection.
  312. =cut
  313. $dRootIn = Pali::D->new ($dRootIn);
  314. $dRootOut = Pali::D->new ($dRootOut);
  315. $dConf = ($dConf
  316. ? Pali::D->new ($dConf)
  317. : Pali::D->new ($dRootIn->getPath, "pageling"));
  318. =head2 Configuration files
  319. Content of the configuration files is read into three variables:
  320. C<%languages>, C<%navigation>, and C<%switcher>.
  321. =cut
  322. my %fConf = (
  323. 'languages' => path ($dConf->getPath, "languages.paliya"),
  324. 'navigation' => path ($dConf->getPath, "navigation.paliya"),
  325. 'switcher' => path ($dConf->getPath, "switcher.paliya"),
  326. );
  327. -e $fConf{languages} or die "Missing language configuration file"
  328. . " ‘$fConf{languages}’";
  329. -e $fConf{navigation} or die "Missing navigation configuration file"
  330. . " ‘$fConf{navigation}’";
  331. -e $fConf{switcher} or die "Missing switcher configuration file"
  332. . " ‘$fConf{switcher}’";
  333. my %languages = %{readPaliya $fConf{languages}};
  334. my %navigation = %{readPaliya $fConf{navigation}};
  335. my %switcher = %{readPaliya $fConf{switcher}};
  336. my @fIn;
  337. find (\&registerFiles, $dRootIn->getPath);
  338. sub registerFiles {
  339. say "‣‣‣ Examining $_" if $verbose >= 4;
  340. if (-d || /(.paliya|\#|\~)$/) {
  341. say "‣‣‣ Not delving into that!" if $verbose >= 4;
  342. return;
  343. } else {
  344. my $f = Pali::F->new ($_);
  345. $f->markIndex if $f =~ /^index\./;
  346. push @fIn, $f;
  347. say "‣‣‣ Got it!" if $verbose >= 4;
  348. }
  349. }
  350. foreach my $lang (keys %languages) {
  351. $dRootIn->setFOut ($lang, $dRootOut);
  352. }
  353. keys %languages; # Reset iterator.
  354. say "Processing input files…" if $verbose >=1;
  355. # find (\&firstPass, $dRootIn);
  356. foreach my $fIn (@fIn) {
  357. say "• File: $fIn" if $verbose >= 2;
  358. my ($lan, $fil, $tit, $nav) = ('', '', '', ''); # Registered directives
  359. my ($fOut, $fhIn, $fhOut);
  360. open $fhIn, '<', $fIn->getPath;
  361. while (my $line = <$fhIn>) {
  362. if ($line =~ s/^:::\s*//) { # This is a directive!
  363. my ($key, $value) = each %{readPaliya (\$line)};
  364. if ($key =~ /^(lan|language|lin|lingvo)$/) {
  365. ($lan, $fil, $tit) = ($value, '', '');
  366. $fIn->addLan ($lan);
  367. ($fIn->parent)->addLan ($lan) if $fIn->isIndex;
  368. say " ◦ Processing language ‘$lan’" if $verbose >= 3;
  369. } elsif ($key =~ /^(fil|file|dos|dosiero)$/) {
  370. $fil = $value if $lan;
  371. my $parent = $fIn->parent;
  372. if ($fIn->isIndex) {
  373. my $grandparent = $parent->parent;
  374. my $fParentOut = path ($grandparent->getFOut ($lan), $fil);
  375. $parent->setFOut ($lan, $fParentOut);
  376. $fOut = path ($parent->getFOut ($lan), "index.html");
  377. } else {
  378. $fOut = path ($parent->getFOut ($lan), $fil);
  379. }
  380. $fIn->setFOut ($lan, $fOut);
  381. open $fhOut, '>', $fOut or die "Couldn't open ‘$fOut’";
  382. say " ◦ Output file set to ‘$fOut’" if $verbose >= 3;
  383. } elsif ($key =~ /^(tit|title|titolo)$/) {
  384. $tit = $value if $lan;
  385. $fIn->setTit ($lan, $tit);
  386. say " ◦ Title set to ‘$tit’" if $verbose >= 3;
  387. } elsif ($key =~ /^(nav|navigation|navigilo)$/) {
  388. $nav = 'yes';
  389. ($fIn->parent)->addNav ($fIn);
  390. say " ◦ Added to parent's navigation" if $verbose >= 3;
  391. } else {
  392. say "Unrecognised directive found in ‘$fIn’!";
  393. }
  394. next;
  395. } else {
  396. if ($fil) {
  397. print $fhOut $line;
  398. }
  399. }
  400. }
  401. }