lsg

Lumidify Site Generator
git clone git://lumidify.org/git/lsg.git
Log | Files | Refs | README | LICENSE

commit 1f51f969de109213a0b92121310444f35ab53e84
Author: lumidify <nobody@lumidify.org>
Date:   Fri, 21 Feb 2020 14:15:30 +0100

Initial Commit

Diffstat:
ALICENSE | 121+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ALSG.pm | 47+++++++++++++++++++++++++++++++++++++++++++++++
ALSG/Config.pm | 103+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ALSG/Generate.pm | 88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ALSG/Markdown.pm | 203+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ALSG/Metadata.pm | 100+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ALSG/Misc.pm | 43+++++++++++++++++++++++++++++++++++++++++++
ALSG/Template.pm | 194+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ALSG/UserFuncs.pm | 110+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
AREADME | 45+++++++++++++++++++++++++++++++++++++++++++++
Agenerate.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();