URI: 
       tInitial Commit - lsg - Lumidify Site Generator
  HTML git clone git://lumidify.org/git/lsg.git
   DIR Log
   DIR Files
   DIR Refs
   DIR README
   DIR LICENSE
       ---
   DIR commit 1f51f969de109213a0b92121310444f35ab53e84
  HTML 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(-)
       ---
   DIR diff --git a/LICENSE b/LICENSE
       t@@ -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.
   DIR diff --git a/LSG.pm b/LSG.pm
       t@@ -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;
   DIR diff --git a/LSG/Config.pm b/LSG/Config.pm
       t@@ -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;
   DIR diff --git a/LSG/Generate.pm b/LSG/Generate.pm
       t@@ -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;
   DIR diff --git a/LSG/Markdown.pm b/LSG/Markdown.pm
       t@@ -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;
   DIR diff --git a/LSG/Metadata.pm b/LSG/Metadata.pm
       t@@ -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;
   DIR diff --git a/LSG/Misc.pm b/LSG/Misc.pm
       t@@ -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;
   DIR diff --git a/LSG/Template.pm b/LSG/Template.pm
       t@@ -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;
   DIR diff --git a/LSG/UserFuncs.pm b/LSG/UserFuncs.pm
       t@@ -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;
   DIR diff --git a/README b/README
       t@@ -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.
   DIR diff --git a/generate.pl b/generate.pl
       t@@ -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();