commit 1f51f969de109213a0b92121310444f35ab53e84
Author: lumidify <nobody@lumidify.org>
Date: Fri, 21 Feb 2020 14:15:30 +0100
Initial Commit
Diffstat:
A | LICENSE | | | 121 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | LSG.pm | | | 47 | +++++++++++++++++++++++++++++++++++++++++++++++ |
A | LSG/Config.pm | | | 103 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | LSG/Generate.pm | | | 88 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | LSG/Markdown.pm | | | 203 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | LSG/Metadata.pm | | | 100 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | LSG/Misc.pm | | | 43 | +++++++++++++++++++++++++++++++++++++++++++ |
A | LSG/Template.pm | | | 194 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | LSG/UserFuncs.pm | | | 110 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | README | | | 45 | +++++++++++++++++++++++++++++++++++++++++++++ |
A | generate.pl | | | 28 | ++++++++++++++++++++++++++++ |
11 files changed, 1082 insertions(+), 0 deletions(-)
diff --git a/LICENSE b/LICENSE
@@ -0,0 +1,121 @@
+Creative Commons Legal Code
+
+CC0 1.0 Universal
+
+ CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
+ LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
+ ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
+ INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
+ REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
+ PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
+ THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
+ HEREUNDER.
+
+Statement of Purpose
+
+The laws of most jurisdictions throughout the world automatically confer
+exclusive Copyright and Related Rights (defined below) upon the creator
+and subsequent owner(s) (each and all, an "owner") of an original work of
+authorship and/or a database (each, a "Work").
+
+Certain owners wish to permanently relinquish those rights to a Work for
+the purpose of contributing to a commons of creative, cultural and
+scientific works ("Commons") that the public can reliably and without fear
+of later claims of infringement build upon, modify, incorporate in other
+works, reuse and redistribute as freely as possible in any form whatsoever
+and for any purposes, including without limitation commercial purposes.
+These owners may contribute to the Commons to promote the ideal of a free
+culture and the further production of creative, cultural and scientific
+works, or to gain reputation or greater distribution for their Work in
+part through the use and efforts of others.
+
+For these and/or other purposes and motivations, and without any
+expectation of additional consideration or compensation, the person
+associating CC0 with a Work (the "Affirmer"), to the extent that he or she
+is an owner of Copyright and Related Rights in the Work, voluntarily
+elects to apply CC0 to the Work and publicly distribute the Work under its
+terms, with knowledge of his or her Copyright and Related Rights in the
+Work and the meaning and intended legal effect of CC0 on those rights.
+
+1. Copyright and Related Rights. A Work made available under CC0 may be
+protected by copyright and related or neighboring rights ("Copyright and
+Related Rights"). Copyright and Related Rights include, but are not
+limited to, the following:
+
+ i. the right to reproduce, adapt, distribute, perform, display,
+ communicate, and translate a Work;
+ ii. moral rights retained by the original author(s) and/or performer(s);
+iii. publicity and privacy rights pertaining to a person's image or
+ likeness depicted in a Work;
+ iv. rights protecting against unfair competition in regards to a Work,
+ subject to the limitations in paragraph 4(a), below;
+ v. rights protecting the extraction, dissemination, use and reuse of data
+ in a Work;
+ vi. database rights (such as those arising under Directive 96/9/EC of the
+ European Parliament and of the Council of 11 March 1996 on the legal
+ protection of databases, and under any national implementation
+ thereof, including any amended or successor version of such
+ directive); and
+vii. other similar, equivalent or corresponding rights throughout the
+ world based on applicable law or treaty, and any national
+ implementations thereof.
+
+2. Waiver. To the greatest extent permitted by, but not in contravention
+of, applicable law, Affirmer hereby overtly, fully, permanently,
+irrevocably and unconditionally waives, abandons, and surrenders all of
+Affirmer's Copyright and Related Rights and associated claims and causes
+of action, whether now known or unknown (including existing as well as
+future claims and causes of action), in the Work (i) in all territories
+worldwide, (ii) for the maximum duration provided by applicable law or
+treaty (including future time extensions), (iii) in any current or future
+medium and for any number of copies, and (iv) for any purpose whatsoever,
+including without limitation commercial, advertising or promotional
+purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
+member of the public at large and to the detriment of Affirmer's heirs and
+successors, fully intending that such Waiver shall not be subject to
+revocation, rescission, cancellation, termination, or any other legal or
+equitable action to disrupt the quiet enjoyment of the Work by the public
+as contemplated by Affirmer's express Statement of Purpose.
+
+3. Public License Fallback. Should any part of the Waiver for any reason
+be judged legally invalid or ineffective under applicable law, then the
+Waiver shall be preserved to the maximum extent permitted taking into
+account Affirmer's express Statement of Purpose. In addition, to the
+extent the Waiver is so judged Affirmer hereby grants to each affected
+person a royalty-free, non transferable, non sublicensable, non exclusive,
+irrevocable and unconditional license to exercise Affirmer's Copyright and
+Related Rights in the Work (i) in all territories worldwide, (ii) for the
+maximum duration provided by applicable law or treaty (including future
+time extensions), (iii) in any current or future medium and for any number
+of copies, and (iv) for any purpose whatsoever, including without
+limitation commercial, advertising or promotional purposes (the
+"License"). The License shall be deemed effective as of the date CC0 was
+applied by Affirmer to the Work. Should any part of the License for any
+reason be judged legally invalid or ineffective under applicable law, such
+partial invalidity or ineffectiveness shall not invalidate the remainder
+of the License, and in such case Affirmer hereby affirms that he or she
+will not (i) exercise any of his or her remaining Copyright and Related
+Rights in the Work or (ii) assert any associated claims and causes of
+action with respect to the Work, in either case contrary to Affirmer's
+express Statement of Purpose.
+
+4. Limitations and Disclaimers.
+
+ a. No trademark or patent rights held by Affirmer are waived, abandoned,
+ surrendered, licensed or otherwise affected by this document.
+ b. Affirmer offers the Work as-is and makes no representations or
+ warranties of any kind concerning the Work, express, implied,
+ statutory or otherwise, including without limitation warranties of
+ title, merchantability, fitness for a particular purpose, non
+ infringement, or the absence of latent or other defects, accuracy, or
+ the present or absence of errors, whether or not discoverable, all to
+ the greatest extent permissible under applicable law.
+ c. Affirmer disclaims responsibility for clearing rights of other persons
+ that may apply to the Work or any use thereof, including without
+ limitation any person's Copyright and Related Rights in the Work.
+ Further, Affirmer disclaims responsibility for obtaining any necessary
+ consents, permissions or other rights required for any use of the
+ Work.
+ d. Affirmer understands and acknowledges that Creative Commons is not a
+ party to this document and has no duty or obligation with respect to
+ this CC0 or use of the Work.
diff --git a/LSG.pm b/LSG.pm
@@ -0,0 +1,47 @@
+#!/usr/bin/env perl
+
+# LSG.pm - Lumidify Site Generator
+# Written by lumidify <nobody@lumidify.org>
+# Last updated: 2019-08-21
+#
+# To the extent possible under law, the author has dedicated
+# all copyright and related and neighboring rights to this
+# software to the public domain worldwide. This software is
+# distributed without any warranty.
+#
+# You should have received a copy of the CC0 Public Domain
+# Dedication along with this software. If not, see
+# <http://creativecommons.org/publicdomain/zero/1.0/>.
+
+# Note: cross-platform path processing is used wherever possible, but
+# other parts won't work properly anyways if the path separator isn't /.
+# Good that nobody important uses any OS on which that's the case.
+
+package LSG;
+use strict;
+use warnings;
+use LSG::Config;
+use LSG::Template;
+use LSG::UserFuncs;
+use LSG::Metadata;
+use LSG::Generate;
+use Data::Dumper;
+
+# FIXME: don't just chdir into $path, in case that messes up anything
+# the calling script wanted to do afterwards
+sub init {
+ my $path = shift;
+ chdir($path) or die "Unable to access directory \"$path\": $!\n";
+ LSG::Config::init_config("config.ini", "modified_dates");
+ LSG::Template::init_templates();
+ LSG::Metadata::init_metadata();
+ LSG::UserFuncs::init_userfuncs();
+}
+
+sub generate_site {
+ LSG::Generate::gen_files();
+ LSG::Generate::delete_obsolete();
+ LSG::Config::write_modified_dates("modified_dates");
+}
+
+1;
diff --git a/LSG/Config.pm b/LSG/Config.pm
@@ -0,0 +1,103 @@
+#!/usr/bin/env perl
+
+# LSG::Config - configuration for the LSG
+# Written by lumidify <nobody@lumidify.org>
+# Last updated: 2019-08-21
+#
+# To the extent possible under law, the author has dedicated
+# all copyright and related and neighboring rights to this
+# software to the public domain worldwide. This software is
+# distributed without any warranty.
+#
+# You should have received a copy of the CC0 Public Domain
+# Dedication along with this software. If not, see
+# <http://creativecommons.org/publicdomain/zero/1.0/>.
+
+package LSG::Config;
+use strict;
+use warnings;
+use utf8;
+use open qw< :encoding(UTF-8) >;
+
+use Exporter qw(import);
+our @EXPORT_OK = qw($config);
+
+# Yes, I know this isn't just used for real config
+our $config;
+
+sub read_modified_dates {
+ my $path = shift;
+ my %dates = (pages => {}, templates => {});
+ if (!-f $path) {
+ print(STDERR "Unable to open \"$path\". Using empty modified_dates.\n");
+ return \%dates;
+ }
+ open (my $fh, "<", $path) or die "Unable to open $path: $!\n";
+ foreach (<$fh>) {
+ chomp;
+ my @fields = split(" ", $_, 2);
+ my $date = $fields[0];
+ my $filename = $fields[1];
+ if ($filename =~ /\Apages\//) {
+ $dates{"pages"}->{substr($filename, 6)} = $date;
+ } elsif ($filename =~ /\Atemplates\//) {
+ $dates{"templates"}->{substr($filename, 10)} = $date;
+ } else {
+ die "Invalid file path \"$filename\" in \"$path\".\n";
+ }
+ }
+ close($fh);
+ return \%dates;
+}
+
+sub write_modified_dates {
+ my $path = shift;
+ open(my $fh, ">", $path) or die "Unable to open \"$path\": $!\n";
+ foreach my $pageid (keys %{$config->{"metadata"}}) {
+ foreach my $lang (keys %{$config->{"metadata"}->{$pageid}->{"modified"}}) {
+ print($fh $config->{"metadata"}->{$pageid}->{"modified"}->{$lang} . " pages/$pageid.$lang\n");
+ }
+ }
+ foreach my $template (keys %{$config->{"templates"}}) {
+ print($fh $config->{"templates"}->{$template}->{"modified"} . " templates/$template\n");
+ }
+ close($fh);
+}
+
+sub read_config {
+ my $path = shift;
+ my %config;
+ open (my $fh, "<", $path) or die "Unable to open $path: #!\n";
+ my $section = "";
+ foreach (<$fh>) {
+ chomp;
+ if ($_ eq "") {
+ $section = "";
+ next;
+ }
+ if (/^\[(.*)\]$/) {
+ $section = $1;
+ next;
+ }
+ my ($key, $value) = split("=", $_, 2);
+ if ($value =~ /:/) {
+ my @value = split(":", $value);
+ $value = \@value;
+ }
+ if ($section) {
+ $config{$section}->{$key} = $value;
+ } else {
+ $config{$key} = $value;
+ }
+ }
+ close($fh);
+ return \%config;
+}
+
+sub init_config {
+ my ($config_path, $modified_path) = @_;
+ $config = read_config($config_path);
+ $config->{"modified_dates"} = read_modified_dates($modified_path);
+}
+
+1;
diff --git a/LSG/Generate.pm b/LSG/Generate.pm
@@ -0,0 +1,88 @@
+#!/usr/bin/env perl
+
+# LSG::Generate - main generation function for the LSG
+# Written by lumidify <nobody@lumidify.org>
+# Last updated: 2019-08-21
+#
+# To the extent possible under law, the author has dedicated
+# all copyright and related and neighboring rights to this
+# software to the public domain worldwide. This software is
+# distributed without any warranty.
+#
+# You should have received a copy of the CC0 Public Domain
+# Dedication along with this software. If not, see
+# <http://creativecommons.org/publicdomain/zero/1.0/>.
+
+package LSG::Generate;
+use strict;
+use warnings;
+use utf8;
+use open qw< :encoding(UTF-8) >;
+binmode(STDOUT, ":utf8");
+use Cwd;
+use File::Spec::Functions qw(catfile);
+use File::Path qw(make_path);
+use LSG::Markdown;
+use LSG::Config qw($config);
+
+sub gen_files() {
+ foreach my $pageid (keys %{$config->{"metadata"}}) {
+ foreach my $lang (keys %{$config->{"langs"}}) {
+ my $template = $config->{"metadata"}->{$pageid}->{"template"} . ".$lang.html";
+ if (
+ exists($config->{"modified_dates"}->{"pages"}->{"$pageid.$lang"}) &&
+ exists($config->{"modified_dates"}->{"templates"}->{$template}) &&
+ $config->{"modified_dates"}->{"pages"}->{"$pageid.$lang"} eq $config->{"metadata"}->{$pageid}->{"modified"}->{$lang} &&
+ $config->{"modified_dates"}->{"templates"}->{$template} eq $config->{"templates"}->{$template}->{"modified"}
+ ) {
+ next;
+ }
+ print("Processing $pageid.$lang\n");
+ my $html_dir = catfile("site", $lang, $config->{"metadata"}->{$pageid}->{"dirname"});
+ make_path($html_dir);
+ my $fullname = catfile("pages", "$pageid.$lang");
+ my $html = LSG::Markdown::parse_md($lang, $pageid, $fullname);
+ my $final_html = LSG::Template::render_template($html, $lang, $pageid);
+ my $html_file = catfile("site", $lang, $pageid) . ".html";
+ open(my $in, ">", $html_file) or die "ERROR: can't open $html_file for writing\n";
+ print $in $final_html;
+ close($in);
+ }
+ }
+}
+
+sub delete_obsolete_recurse {
+ my $dir = shift;
+ opendir(my $dh, $dir) or die "Unable to open directory \"" . getcwd() . "/$dir\": $!\n";
+ my $filename;
+ my @dirs;
+ while ($filename = readdir($dh)) {
+ next if $filename =~ /\A\.\.?\z/;
+ my $path = $dir eq "." ? $filename : catfile($dir, $filename);
+ if (-d $path) {
+ push(@dirs, $path);
+ next;
+ }
+ my $pageid = $path;
+ $pageid =~ s/\.html\z//;
+ if (!exists($config->{"metadata"}->{$pageid})) {
+ print("Deleting old file \"" . getcwd() . "/$path\".\n");
+ unlink($path);
+ }
+ }
+ closedir($dh);
+ foreach (@dirs) {
+ delete_obsolete_recurse($_);
+ }
+}
+
+sub delete_obsolete {
+ my $cur = getcwd();
+ foreach my $lang (keys %{$config->{"langs"}}) {
+ chdir(catfile("site", $lang)) or die "Unable to access directory \"site/$lang\": $!\n";
+ delete_obsolete_recurse(".");
+ chdir($cur);
+ }
+}
+
+1;
diff --git a/LSG/Markdown.pm b/LSG/Markdown.pm
@@ -0,0 +1,203 @@
+#!/usr/bin/env perl
+
+# LSG::Markdown - markdown preprocessor for the LSG
+# Written by lumidify <nobody@lumidify.org>
+# Last updated: 2019-08-21
+#
+# To the extent possible under law, the author has dedicated
+# all copyright and related and neighboring rights to this
+# software to the public domain worldwide. This software is
+# distributed without any warranty.
+#
+# You should have received a copy of the CC0 Public Domain
+# Dedication along with this software. If not, see
+# <http://creativecommons.org/publicdomain/zero/1.0/>.
+
+package LSG::Markdown;
+use strict;
+use warnings;
+use utf8;
+use open qw< :encoding(UTF-8) >;
+use File::Spec::Functions;
+use Text::Markdown qw(markdown);
+use LSG::Misc;
+use LSG::Config qw($config);
+
+sub handle_fnc {
+ my $pageid = shift;
+ my $lang = shift;
+ my $line = shift;
+ my $file = shift;
+ my $fnc_name = shift;
+ my @fnc_args = split(/ /, shift);
+ if (!exists($config->{"funcs"}->{$fnc_name})) {
+ die "ERROR: $file: undefined function \"$fnc_name\":\n$line\n";
+ }
+ return $config->{"funcs"}->{$fnc_name}->($pageid, $lang, @fnc_args);
+}
+
+sub handle_lnk {
+ my $pageid = shift;
+ my $lang = shift;
+ my $line = shift;
+ my $file = shift;
+ my $txt = shift;
+ my $lnk = shift;
+ my $lnk_file = "";
+ my $lnk_path = "";
+ my $url = "";
+
+ my $char_one = substr($lnk, 0, 1);
+ if ($char_one eq "@") {
+ $lnk_file = $config->{"metadata"}->{$pageid}->{"basename"} . substr($lnk, 1);
+ $lnk_path = catfile("site", "static", $lnk_file);
+ $url = LSG::Misc::gen_relative_link("$lang/$pageid", "static/$lnk_file");
+ } elsif ($char_one eq "#") {
+ $lnk_file = substr($lnk, 1);
+ $lnk_path = catfile("site", "static", $lnk_file);
+ $url = LSG::Misc::gen_relative_link("$lang/$pageid", "static/$lnk_file");
+ } elsif ($char_one eq "\$") {
+ $lnk_file = substr($lnk, 1);
+ $lnk_path = catfile("pages", $lnk_file);
+ # Convert to /lang/page format
+ my $lnk_abs = substr($lnk_file, -2) . "/" . substr($lnk_file, 0, length($lnk_file) - 3) . ".html";
+ $url = LSG::Misc::gen_relative_link("$lang/$pageid", $lnk_abs);
+ } else {
+ $url = $lnk;
+ }
+ if ($lnk_path && !(-f $lnk_path)) {
+ die "ERROR: $file: linked file $lnk_path does not exist:\n$line\n";
+ }
+ return "[$txt]($url)";
+}
+
+sub handle_img {
+ my $pageid = shift;
+ my $lang = shift;
+ my $line = shift;
+ my $file = shift;
+ my $txt = shift;
+ my $img = shift;
+ my $img_file = "";
+ my $img_path = "";
+ my $src = "";
+
+ my $char_one = substr($img, 0, 1);
+ if ($char_one eq "@") {
+ $img_file = $config->{"metadata"}->{$pageid}->{"basename"} . substr($img, 1);
+ $img_path = catfile("site", "static", $img_file);
+ $src = LSG::Misc::gen_relative_link("$lang/$pageid", "static/$img_file");
+ } elsif ($char_one eq "#") {
+ $img_file = substr($img, 1);
+ $img_path = catfile("site", "static", $img_file);
+ $src = LSG::Misc::gen_relative_link("$lang/$pageid", "static/$img_file");
+ } else {
+ $src = $img;
+ }
+ if ($img_path && !(-f $img_path)) {
+ die "ERROR: $file: image file $img_path does not exist:\n$line\n";
+ }
+
+ return "![$txt]($src)";
+}
+
+sub add_child {
+ my $parent = shift;
+ my $type = shift;
+ $parent->{"child"} = {type => $type, txt => "", url => "", parent => $parent, child => {}};
+
+ return $parent->{"child"};
+}
+
+sub finish_child {
+ my $child = shift;
+ my $pageid = shift;
+ my $lang = shift;
+ my $line = shift;
+ my $file = shift;
+ my $parent = $child->{"parent"};
+
+ if ($child->{"type"} eq "img") {
+ $parent->{"txt"} .= handle_img($pageid, $lang, $line, $file, $child->{"txt"}, $child->{"url"});
+ } elsif ($child->{"type"} eq "lnk") {
+ $parent->{"txt"} .= handle_lnk($pageid, $lang, $line, $file, $child->{"txt"}, $child->{"url"});
+ } elsif ($child->{"type"} eq "fnc") {
+ $parent->{"txt"} .= handle_fnc($pageid, $lang, $line, $file, $child->{"txt"}, $child->{"url"});
+ }
+
+ return $parent;
+}
+
+sub parse_md {
+ my $lang = shift;
+ my $pageid = shift;
+ my $inpath = shift;
+ open(my $in, "<", $inpath) or die "ERROR: Can't open $inpath for reading.";
+ # skip metadata
+ while (<$in> =~ /^([^:]*):(.*)$/) {}
+
+ my $txt = "";
+ my $bs = 0;
+ my $IN_IMG = 1;
+ my $IN_LNK = 2;
+ my $IN_FNC = 4;
+ my $IN_TXT = 8;
+ my $IN_URL = 16;
+ my $IN_IMG_START = 32;
+ my %structure = (txt => "", child => {});
+ my $cur_child_ref = \%structure;
+ my @states = (0);
+ foreach (<$in>) {
+ foreach my $char (split //, $_) {
+ if ($char eq "\\") {
+ $bs++;
+ if (!($bs %= 2)) {$txt .= "\\"};
+ } elsif ($bs % 2) {
+ # FIXME: CLEANUP!!!
+ if ($states[-1] & $IN_TXT) {
+ $cur_child_ref->{"txt"} .= $char;
+ } elsif ($states[-1] & $IN_URL) {
+ $cur_child_ref->{"url"} .= $char;
+ } elsif (!($states[-1] & ($IN_IMG | $IN_LNK | $IN_FNC))) {
+ $structure{"txt"} .= $char;
+ }
+ $bs = 0;
+ } elsif ($char eq "!") {
+ push(@states, $IN_IMG_START);
+ } elsif ($char eq "[") {
+ if ($states[-1] & $IN_IMG_START) {
+ $states[-1] = $IN_IMG | $IN_TXT;
+ $cur_child_ref = add_child($cur_child_ref, "img");
+ } else {
+ push(@states, $IN_LNK | $IN_TXT);
+ $cur_child_ref = add_child($cur_child_ref, "lnk");
+ }
+ } elsif ($char eq "{") {
+ $cur_child_ref = add_child($cur_child_ref, "fnc");
+ push(@states, $IN_FNC | $IN_TXT);
+ } elsif ($char eq "]" && ($states[-1] & ($IN_IMG | $IN_LNK) && $states[-1] & $IN_TXT)) {
+ $states[-1] &= ~$IN_TXT;
+ } elsif ($char eq "}" && $states[-1] & $IN_FNC && $states[-1] & $IN_TXT) {
+ $states[-1] &= ~$IN_TXT;
+ } elsif ($char eq "(" && $states[-1] & ($IN_IMG | $IN_LNK | $IN_FNC)) {
+ $states[-1] |= $IN_URL;
+ } elsif ($char eq ")" && ($states[-1] & $IN_URL)) {
+ pop(@states);
+ $cur_child_ref = finish_child($cur_child_ref, $pageid, $lang, $_, $inpath);
+ } else {
+ if ($states[-1] & $IN_IMG_START) {pop(@states)}
+ if ($states[-1] & $IN_TXT) {
+ $cur_child_ref->{"txt"} .= $char;
+ } elsif ($states[-1] & $IN_URL) {
+ $cur_child_ref->{"url"} .= $char;
+ } elsif (!($states[-1] & ($IN_IMG | $IN_LNK | $IN_FNC))) {
+ $structure{"txt"} .= $char;
+ }
+ }
+ }
+ }
+
+ return markdown($structure{"txt"});
+}
+
+1;
diff --git a/LSG/Metadata.pm b/LSG/Metadata.pm
@@ -0,0 +1,100 @@
+#!/usr/bin/env perl
+
+# LSG::Metadata - metadata parser for the LSG
+# Written by lumidify <nobody@lumidify.org>
+# Last updated: 2019-08-21
+#
+# To the extent possible under law, the author has dedicated
+# all copyright and related and neighboring rights to this
+# software to the public domain worldwide. This software is
+# distributed without any warranty.
+#
+# You should have received a copy of the CC0 Public Domain
+# Dedication along with this software. If not, see
+# <http://creativecommons.org/publicdomain/zero/1.0/>.
+
+package LSG::Metadata;
+use strict;
+use warnings;
+use utf8;
+use open qw< :encoding(UTF-8) >;
+use File::Find;
+use File::Spec::Functions qw(catfile catdir splitdir);
+use File::Path;
+use LSG::Config qw($config);
+
+sub parse_metadata_file {
+ my $in = shift;
+ my %tmp_fm = ();
+ while (<$in> =~ /^([^:]*):(.*)$/) {
+ $tmp_fm{$1} = $2;
+ if (eof) {last}
+ }
+ return \%tmp_fm;
+}
+
+sub parse_metadata {
+ if (!(-f)) {return};
+ my $fullname = $File::Find::name;
+ # Strip "pages/" from dirname
+ my @dirs = splitdir($File::Find::dir);
+ my $dirname = catdir(@dirs[1..$#dirs]);
+ my $basename = substr($_, 0, $#_-2);
+ # Note: this will only work if language codes are two chars
+ my $lang = substr($_, -2);
+ my $pageid = $basename;
+ if ($dirname) {$pageid = catfile($dirname, $basename)};
+ open(my $in, "<", $_) or die "Can't open $fullname: $!";
+ my %tmp_md = %{parse_metadata_file($in)};
+ close($in);
+ my $modified_date = (stat($_))[9];
+ $config->{"metadata"}->{$pageid}->{"modified"}->{$lang} = $modified_date;
+ if (!exists($tmp_md{"template"})) {
+ die "ERROR: $fullname does not specify a template\n";
+ }
+ if (!exists($config->{"templates"}->{$tmp_md{"template"} . ".$lang.html"})) {
+ die "ERROR: $fullname: template " . $tmp_md{"template"} . " does not exist\n";
+ }
+ # Note: if different templates are specified for different languages,
+ # the one from the last language analyzed is used.
+ # FIXME: change this - if different templates are specified for different langs,
+ # the template isn't checked for existance in other langs
+ # Wait, why not just use the actual template given? It's stored in $config->{"metadata"} anyways
+ $config->{"metadata"}->{$pageid}->{"template"} = $tmp_md{"template"};
+ foreach my $md_id (split / /, $config->{"templates"}->{$tmp_md{"template"} . ".$lang.html"}->{"metadata"}) {
+ if (!exists($tmp_md{$md_id})) {
+ die "ERROR: $fullname does not include \"$md_id\" metadata\n";
+ }
+ }
+ foreach my $md_id (keys %tmp_md) {
+ $config->{"metadata"}->{$pageid}->{$lang}->{$md_id} = $tmp_md{$md_id};
+ }
+ $config->{"metadata"}->{$pageid}->{"dirname"} = $dirname;
+ $config->{"metadata"}->{$pageid}->{"basename"} = $basename;
+}
+
+sub gen_metadata_hash {
+ find(\&parse_metadata, "pages/");
+}
+
+sub check_metadata_langs {
+ my $not_found;
+ foreach my $pageid (keys %{$config->{"metadata"}}) {
+ $not_found = "";
+ foreach my $lang (keys %{$config->{"langs"}}) {
+ if (!exists($config->{"metadata"}->{$pageid}->{$lang})) {
+ $not_found .= " $lang";
+ }
+ }
+ if ($not_found) {
+ die("ERROR: languages \"$not_found\" not found for $pageid\n");
+ }
+ }
+}
+
+sub init_metadata {
+ gen_metadata_hash();
+ check_metadata_langs();
+}
+
+1;
diff --git a/LSG/Misc.pm b/LSG/Misc.pm
@@ -0,0 +1,43 @@
+#!/usr/bin/env perl
+
+# LSG::Misc - miscellaneous functions for the LSG
+# Written by lumidify <nobody@lumidify.org>
+# Last updated: 2019-08-21
+#
+# To the extent possible under law, the author has dedicated
+# all copyright and related and neighboring rights to this
+# software to the public domain worldwide. This software is
+# distributed without any warranty.
+#
+# You should have received a copy of the CC0 Public Domain
+# Dedication along with this software. If not, see
+# <http://creativecommons.org/publicdomain/zero/1.0/>.
+
+package LSG::Misc;
+use strict;
+use warnings;
+use utf8;
+use open qw< :encoding(UTF-8) >;
+
+# Generate relative link - both paths must already be relative,
+# starting at the same place!
+# e.g. "bob/hi/whatever/meh.txt","bob/hi/bla/fred.txt" => ../bla/fred.txt
+sub gen_relative_link {
+ my ($base, $linked) = @_;
+ my @parts_base = split("/", $base);
+ my @parts_linked = split("/", $linked);
+ # don't include last element in @parts_base (the filename)
+ my $i = 0;
+ while ($i < $#parts_base && $i < $#parts_linked) {
+ if ($parts_base[$i] ne $parts_linked[$i]) {
+ last;
+ }
+ $i++;
+ }
+ my $rel_lnk = "";
+ $rel_lnk .= "../" x ($#parts_base-$i);
+ $rel_lnk .= join("/", @parts_linked[$i..$#parts_linked]);
+ return $rel_lnk;
+}
+
+1;
diff --git a/LSG/Template.pm b/LSG/Template.pm
@@ -0,0 +1,194 @@
+#!/usr/bin/env perl
+
+# LSG::Template - template processor for the LSG
+# Written by lumidify <nobody@lumidify.org>
+# Last updated: 2019-08-21
+#
+# To the extent possible under law, the author has dedicated
+# all copyright and related and neighboring rights to this
+# software to the public domain worldwide. This software is
+# distributed without any warranty.
+#
+# You should have received a copy of the CC0 Public Domain
+# Dedication along with this software. If not, see
+# <http://creativecommons.org/publicdomain/zero/1.0/>.
+
+package LSG::Template;
+use strict;
+use warnings;
+use utf8;
+use open qw< :encoding(UTF-8) >;
+use File::Spec::Functions qw(catfile);
+use Storable 'dclone';
+use LSG::Config qw($config);
+use LSG::Metadata;
+
+sub parse_template {
+ my $template_name = shift;
+ my $state = 0;
+ my $IN_BRACE = 1;
+ my $IN_BLOCK = 2;
+ my $txt = "";
+ my $bs = 0;
+
+ # Note: there needs to be a line between metadata and content since the
+ # metadata parser takes a line to realize it is not in fm anymore
+ my $inpath = catfile("templates", $template_name);
+ open(my $in, "<", $inpath) or die "ERROR: template: Can't open $inpath for reading.";
+ my $template = LSG::Metadata::parse_metadata_file($in);
+
+ foreach (<$in>) {
+ foreach my $char (split //, $_) {
+ if ($char eq "\\") {
+ $bs++;
+ if (!($bs %= 2)) {$txt .= "\\"};
+ } elsif ($bs % 2) {
+ $txt .= $char;
+ $bs = 0;
+ } elsif ($char eq "{" && !($state & $IN_BRACE)) {
+ $state |= $IN_BRACE;
+ if ($txt ne "") {
+ if ($state & $IN_BLOCK) {
+ push(@{$template->{"contents"}->[-1]->{"contents"}},
+ {type => "txt", contents => $txt});
+ } else {
+ push(@{$template->{"contents"}}, {type => "txt", contents => $txt});
+ }
+ $txt = "";
+ }
+ } elsif ($char eq "}" && $state & $IN_BRACE) {
+ $state &= ~$IN_BRACE;
+ my @brace = split(/ /, $txt);
+ if (!@brace) {
+ die("ERROR: empty brace in $inpath:\n$_\n");
+ } else {
+ if ($brace[0] eq "endblock") {
+ $state &= ~$IN_BLOCK
+ } elsif ($brace[0] eq "block") {
+ $state |= $IN_BLOCK;
+ if ($#brace != 1) {
+ die("ERROR: wrong number of arguments for block in $inpath\n");
+ } else {
+ push(@{$template->{"contents"}}, {type => $brace[0],
+ id => $brace[1],
+ contents => []});
+ }
+ } else {
+ my %tmp = (type => $brace[0]);
+ if ($#brace > 0) {
+ @{$tmp{"args"}} = @brace[1..$#brace];
+ }
+ if ($state & $IN_BLOCK) {
+ push(@{$template->{"contents"}->[-1]->{"contents"}}, \%tmp);
+ } else {
+ push(@{$template->{"contents"}}, \%tmp);
+ }
+ }
+ }
+ $txt = "";
+ } else {
+ $txt .= $char;
+ }
+ }
+ }
+ if ($state & ($IN_BRACE | $IN_BLOCK)) {
+ die("ERROR: unclosed block or brace in $inpath\n");
+ } elsif ($txt ne "") {
+ push(@{$template->{"contents"}}, {type => "txt", contents => $txt});
+ }
+ close($in);
+ my $modified_date = (stat($inpath))[9];
+ $template->{"modified"} = $modified_date;
+ return $template;
+}
+
+sub handle_parent_template {
+ my $parentid = shift;
+ my $childid = shift;
+ if (exists $config->{"templates"}->{$parentid}->{"extends"}) {
+ handle_parent_template($config->{"templates"}->{$parentid}->{"extends"}, $parentid);
+ }
+ if ($config->{"templates"}->{$parentid}->{"modified"} > $config->{"templates"}->{$childid}->{"modified"}) {
+ $config->{"templates"}->{$childid}->{"modified"} = $config->{"templates"}->{$parentid}->{"modified"};
+ }
+ my $parent = $config->{"templates"}->{$parentid}->{"contents"};
+ my $child = $config->{"templates"}->{$childid}->{"contents"};
+ my $child_new = dclone($parent);
+ # Replace blocks from parent template with child blocks
+ # Not very efficient...
+ foreach my $item (@{$child_new}) {
+ if ($item->{"type"} eq "block") {
+ foreach my $item_new (@{$child}) {
+ if ($item_new->{"type"} eq "block" && $item_new->{"id"} eq $item->{"id"}) {
+ $item->{"contents"} = $item_new->{"contents"};
+ last;
+ }
+ }
+ }
+ }
+ $config->{"templates"}->{$childid}->{"contents"} = $child_new;
+ delete $config->{"templates"}->{$childid}->{"extends"};
+}
+
+sub do_template_inheritance {
+ foreach my $template_id (keys %{$config->{"templates"}}) {
+ if (exists $config->{"templates"}->{$template_id}->{"extends"}) {
+ handle_parent_template($config->{"templates"}->{$template_id}->{"extends"}, $template_id);
+ }
+ }
+}
+
+sub init_templates {
+ opendir(my $dir, "templates") or die "ERROR: couldn't open dir templates/\n";
+ my @files = grep {!/\A\.\.?\z/} readdir($dir);
+ closedir($dir);
+ foreach my $filename (@files) {
+ $config->{"templates"}->{$filename} = parse_template($filename);
+ }
+ do_template_inheritance();
+}
+
+# FIXME: more error checking - arg numbers
+# -> not too important though since these are just templates (won't be edited too often)
+sub do_template_items {
+ my $main_content = shift;
+ my $lang = shift;
+ my $pageid = shift;
+ my $template = shift;
+ my $final = "";
+ for my $item (@{$template->{"contents"}}) {
+ if ($item->{"type"} eq "txt") {
+ $final .= $item->{"contents"};
+ } elsif ($item->{"type"} eq "var") {
+ $final .= $config->{"metadata"}->{$pageid}->{$lang}->{$item->{"args"}->[0]};
+ } elsif ($item->{"type"} eq "content") {
+ $final .= $main_content;
+ } elsif ($item->{"type"} eq "block") {
+ $final .= do_template_items($main_content, $lang, $pageid, $item);
+ } elsif ($item->{"type"} eq "func") {
+ my $func = $item->{"args"}->[0];
+ my @func_args = @{$item->{"args"}}[1..$#{$item->{"args"}}];
+ # Pass in the array rather than a reference, so these arguments
+ # are received like all other arguments
+ if (!exists($config->{"funcs"}->{$func})) {
+ # FIXME: need more information to give for error
+ die "ERROR: undefined function \"$func\" in template.\n";
+ }
+ $final .= $config->{"funcs"}->{$func}->($pageid, $lang, @func_args);
+ }
+ }
+ return $final;
+}
+
+sub render_template {
+ my $html = shift;
+ my $lang = shift;
+ my $pageid = shift;
+ my $template = $config->{"metadata"}->{$pageid}->{"template"};
+ if (!exists($config->{"templates"}->{"$template.$lang.html"})) {
+ die "ERROR: can't open template $template.$lang.html\n";
+ }
+ return do_template_items($html, $lang, $pageid, $config->{"templates"}->{"$template.$lang.html"});
+}
+
+1;
diff --git a/LSG/UserFuncs.pm b/LSG/UserFuncs.pm
@@ -0,0 +1,110 @@
+#!/usr/bin/env perl
+
+#TODO: template - func processed once and func processed for each page
+
+# LSG::UserFuncs - user functions for the LSG (called from templates and markdown files)
+# Written by lumidify <nobody@lumidify.org>
+# Last updated: 2019-08-21
+#
+# To the extent possible under law, the author has dedicated
+# all copyright and related and neighboring rights to this
+# software to the public domain worldwide. This software is
+# distributed without any warranty.
+#
+# You should have received a copy of the CC0 Public Domain
+# Dedication along with this software. If not, see
+# <http://creativecommons.org/publicdomain/zero/1.0/>.
+
+package LSG::UserFuncs;
+use strict;
+use warnings;
+use utf8;
+use open qw< :encoding(UTF-8) >;
+use LSG::Config qw($config);
+use LSG::Misc;
+
+# FIXME: maybe also pass line for better error messages
+# Module arguments:
+# 1: page id in %fm
+# 2: page language
+# 3-: other args (e.g. for func call)
+
+sub sort_books {
+ my $pageid = shift;
+ my $lang = shift;
+ my $sort_by = shift;
+ my $create_subheadings = shift;
+ if (!$sort_by) {die "ERROR: not enough arguments to function call in $pageid\n"}
+ my $output = "";
+ my %tmp_md = ();
+ foreach my $id (keys %{$config->{"metadata"}}) {
+ if ($config->{"metadata"}->{$id}->{"dirname"} eq "books") {
+ $tmp_md{$id} = $config->{"metadata"}->{$id};
+ if (!exists($config->{"metadata"}->{$id}->{$lang}->{$sort_by})) {
+ die "ERROR: $pageid: can't sort by \"$sort_by\"\n";
+ }
+ }
+ }
+ my $current = "";
+ foreach my $id (sort {$tmp_md{$a}->{$lang}->{$sort_by} cmp $tmp_md{$b}->{$lang}->{$sort_by} or
+ $tmp_md{$a}->{$lang}->{"title"} cmp $tmp_md{$b}->{$lang}->{"title"}} (keys %tmp_md)) {
+ if ($create_subheadings && $create_subheadings eq "true" && $current ne $tmp_md{$id}->{$lang}->{$sort_by}) {
+ $current = $tmp_md{$id}->{$lang}->{$sort_by};
+ $output .= "<h3>$current</h3>\n";
+ }
+ my $rel_lnk = LSG::Misc::gen_relative_link("$lang/$pageid", "$lang/$id.html");
+ $output .= "<p><a href=\"$rel_lnk\">" . $tmp_md{$id}->{$lang}->{"title"} . "</a></p>\n";
+ }
+
+ return $output;
+}
+
+sub gen_lang_selector {
+ my $pageid = shift;
+ my $lang = shift;
+ my $output = "<ul>\n";
+ foreach my $nav_lang (keys %{$config->{"langs"}}) {
+ if ($nav_lang ne $lang) {
+ my $url = LSG::Misc::gen_relative_link("$lang/$pageid", "$nav_lang/$pageid.html");
+ $output .= "<li><a href=\"$url\">" . $config->{"langs"}->{$nav_lang} . "</a></li>\n";
+ }
+ }
+ $output .= "</ul>";
+
+ return $output;
+}
+
+sub gen_nav {
+ my $pageid = shift;
+ my $lang = shift;
+ # Don't print <ul>'s so extra content can be added in template
+ #my $output = "<ul>\n";
+ my $output = "";
+ my @nav = @{$config->{"nav"}};
+ # Not necessary because of direction: rtl in style
+ #if ($lang_dirs{$lang} eq "rtl") {
+ # @nav = reverse(@nav);
+ #}
+ foreach my $nav_page (@nav) {
+ my $title = $config->{"metadata"}->{$nav_page}->{$lang}->{"title"};
+ my $url = LSG::Misc::gen_relative_link("$lang/$pageid", "$lang/$nav_page.html");
+ $output .= "<li><a href=\"$url\">$title</a></li>\n";
+ }
+ #$output .= "</ul>";
+
+ return $output;
+}
+
+sub gen_relative_link {
+ my ($pageid, $lang, $link) = @_;
+ return LSG::Misc::gen_relative_link("$lang/$pageid", $link);
+}
+
+sub init_userfuncs {
+ $config->{"funcs"}->{"gen_lang_selector"} = \&gen_lang_selector;
+ $config->{"funcs"}->{"sort_books"} = \&sort_books;
+ $config->{"funcs"}->{"gen_nav"} = \&gen_nav;
+ $config->{"funcs"}->{"gen_relative_link"} = \&gen_relative_link;
+}
+
+1;
diff --git a/README b/README
@@ -0,0 +1,45 @@
+Almost all standard markdown features (https://daringfireball.net/projects/markdown/syntax)
+should be supported since the markdown is just passed to the standard markdown parser after
+being preprocessed to make things easier to write.
+
+Notable changes:
+- Link titles and alt text for images is not supported in links that need to be preprocessed,
+ e.g. [Hi](@example.com "Title")
+- Reference-style links are not parsed by the preprocessor
+
+Special simplifications handled by the preprocessor:
+
+Links:
+[Whatever](@.pdf)-> [Whatever](relative/path/to/static/$name_of_page.pdf)
+[Whatever](#bob.pdf) -> [Whatever](relative/path/to/static/bob.pdf)
+[Whatever]($page.en) -> [Whatever](relative/path/to/en/page.html)
+
+Images:
+![Whatever](@.png)-> [Whatever](relative/path/to/static/$name_of_page.png)
+![Whatever](#bob.png) -> [Whatever](relative/path/to/static/bob.png)
+
+Functions:
+Functions can be used for more advanced features. They are written using Perl in the file
+`LSG/UserFuncs.pm` and can be called from a markdown file as follows:
+`{name_of_function}(argument1 argument2 argument3)`
+Note: this format may change in the future if more advanced arguments are needed.
+
+Currently implemented functions:
+
+`sort_books`
+Parameters:
+- attribute to sort by
+- create heading when attribute changes or not
+Purpose:
+Generate sorted list of all books, first by the given attribute, which can be anything
+in the metadata, then by the titles. The second attribute can be used to create, for
+instance, category titles. This does not make sense though when the attribute is just
+the title which changes every time anyways. If the second argument is left out, it
+defaults to "false". The attribute to be sorted by (obviously) needs to be defined for
+each book.
+Example:
+{sort_books}(category false)
+
+Two more functions, `gen_nav` and `gen_lang_selector`, are defined, but they are
+currently only used internally in the templates and probably aren't needed for the
+actual pages.
diff --git a/generate.pl b/generate.pl
@@ -0,0 +1,28 @@
+#!/usr/bin/env perl
+
+# FIXME: standardize var names (e.g. $pageid, $page)
+
+# REQUIREMENTS: Text::Markdown
+
+# lsg.pl - Lumidify Site Generator
+# Written by lumidify <nobody@lumidify.org>
+# Last updated: 2019-08-21
+#
+# To the extent possible under law, the author has dedicated
+# all copyright and related and neighboring rights to this
+# software to the public domain worldwide. This software is
+# distributed without any warranty.
+#
+# You should have received a copy of the CC0 Public Domain
+# Dedication along with this software. If not, see
+# <http://creativecommons.org/publicdomain/zero/1.0/>.
+
+use strict;
+use warnings;
+use FindBin;
+use lib "$FindBin::Bin";
+use LSG;
+
+my $path = $#ARGV >= 0 ? $ARGV[0] : ".";
+LSG::init($path);
+LSG::generate_site();