lektura

lektura.git
git clone git://git.lenczewski.org/lektura.git
Log | Files | Refs

commit 4456fcf33d07d214437552da9aea6dd524bbe3ae
parent 506327e34f6168796ca67aa701746e41e305c323
Author: MikoĊ‚aj Lenczewski <mblenczewski@gmail.com>
Date:   Sun, 12 Apr 2026 22:46:13 +0100

Built out layout, added index generation, improved templating

Diffstat:
M.editorconfig | 8++++----
M.gitignore | 11+++++++++++
Abuild.sh | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aclean.sh | 5+++++
Dindex.html | 76----------------------------------------------------------------------------
Dlayout.css | 88-------------------------------------------------------------------------------
Dlogo.svg | 8--------
Dnavbar.css | 58----------------------------------------------------------
Asrc/css/article.css | 8++++++++
Asrc/css/layout.css | 112+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/css/navbar.css | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/css/style.css | 48++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/html/science/article.html | 16++++++++++++++++
Asrc/html/society/article.html | 16++++++++++++++++
Asrc/html/sport/article.html | 16++++++++++++++++
Rscript.js -> src/js/script.js | 0
Rmenu.svg -> src/res/menu.svg | 0
Dstyle.css | 37-------------------------------------
Atemplates/article.html | 27+++++++++++++++++++++++++++
Atemplates/global.html | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atemplates/index.html | 5+++++
Atemplates/listed-article.html | 24++++++++++++++++++++++++
Atemplates/listitem.html | 1+
Atools/convert.py | 16++++++++++++++++
Atools/index.py | 128+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atools/template.py | 149+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atools/utils.py | 3+++
27 files changed, 821 insertions(+), 271 deletions(-)

diff --git a/.editorconfig b/.editorconfig @@ -5,14 +5,14 @@ end_of_line = lf insert_final_newline = true charset = utf-8 -[*.{c,h}] -indent_style = tab -indent_size = 8 - [*.{sh}] indent_style = tab indent_size = 8 +[*.{py}] +indent_style = space +indent_size = 4 + [*.{html,css,js}] indent_style = space indent_size = 2 diff --git a/.gitignore b/.gitignore @@ -1 +1,12 @@ +# build artefacts +gen/ +out/ + +log* + +# python artefacts +**/__pycache__/ + +# per-user files **/.*.swp +tags diff --git a/build.sh b/build.sh @@ -0,0 +1,61 @@ +#!/bin/sh + +CATEGORIES="science society sport" + +set -ex + +mkdir -p gen out + +# copy over static assets +cp -rv src/css/ src/js/ src/res/ public/ out/ + +# template category pages +for category in $CATEGORIES; do + mkdir -p gen/$category out/$category + + # template category article pages + for article in src/html/$category/*.html; do + name="$(basename -s .html $article)" + + # template article snippet + ./tools/template.py \ + $article \ + gen/$category/$name.list.html \ + templates/listed-article.html \ + url="/$category/$name.html" + + # template full article + ./tools/template.py \ + $article \ + gen/$category/$name.html \ + templates/article.html + + ./tools/template.py \ + gen/$category/$name.html \ + out/$category/$name.html \ + templates/global.html + + done + + # generate category index page fragment + ./tools/index.py \ + gen/$category.index.html \ + templates/listitem.html \ + templates/index.html \ + "/$category" \ + gen/$category/*.list.html +done + +# template main page +./tools/index.py \ + gen/index.html \ + templates/listitem.html \ + templates/index.html \ + "/" \ + gen/*.index.html + +# template static pages +./tools/template.py gen/index.html out/index.html templates/global.html title="Lektura" +./tools/template.py gen/science.index.html out/science.html templates/global.html title="Lektura: Science" +./tools/template.py gen/society.index.html out/society.html templates/global.html title="Lektura: Society" +./tools/template.py gen/sport.index.html out/sport.html templates/global.html title="Lektura: Sport" diff --git a/clean.sh b/clean.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +set -ex + +rm -rf gen/ out/ diff --git a/index.html b/index.html @@ -1,76 +0,0 @@ -<!DOCTYPE html> -<html lang="en"> - <head> - <title>Lektura</title> - - <meta charset="utf8" /> - <meta name="viewport" content="initial-scale=1.0, width=device-width" /> - - <link rel="stylesheet" href="/style.css" /> - <link rel="stylesheet" href="/navbar.css" /> - <link rel="stylesheet" href="/layout.css" /> - </head> - <body> - <header> - <div class="center pad-1em"> - <img style="max-width: 2em" src="/logo.svg" /> - </div> - <hr /> - <nav id="global-menu" class="center pad-1em navbar"> - <input id="global-menu-toggle" type="checkbox" class="hidden navbar-toggle" /> - <ul class="navbar-menu"> - <li> - <label class="navbar-toggle-label" for="global-menu-toggle"> - <img src="menu.svg" /> - </label> - </li> - <li><a href="index.html">Index</a></li> - <li><a href="science.html">Science</a></li> - <li><a href="sport.html">Sport</a></li> - <li><a href="society.html">Society</a></li> - </ul> - </nav> - <hr /> - </header> - <main class="grid center"> - <div id="banner" class="grid"> - <div id="breaking-news"> - One - </div> - <div id="breaking-news-1"> - Two - </div> - <div id="breaking-news-2"> - Three - </div> - <div id="breaking-news-3"> - Four - </div> - </div> - <div id="content"> - Lorem Ipsum Dolor Sit Amet. - </div> - <div id="sidebar"> - <section id="recent"> - <h3>Recent</h3> - <ol> - <li><a href="recent1.html">Recent 1</a></li> - <li><a href="recent2.html">Recent 2</a></li> - <li><a href="recent3.html">Recent 3</a></li> - </ol> - </section> - <section id="polls"> - <h3>Polls</h3> - <ol> - <li><a href="poll1.html">Poll 1</a></li> - <li><a href="poll2.html">Poll 2</a></li> - <li><a href="poll3.html">Poll 3</a></li> - </ol> - </section> - </div> - </main> - <footer> - </footer> - </body> - <script src="/script.js"></script> -</html> diff --git a/layout.css b/layout.css @@ -1,88 +0,0 @@ -main.grid { - grid-template-rows: minmax(8em, 16em) minmax(8em, 16em) auto; /* TODO */ - grid-template-columns: 3fr 1fr; -} - -#banner { - height: 8em; -} - -#banner.grid { - grid-template-rows: 1fr 1fr; - grid-template-columns: 3fr 1fr 1fr; -} - -#banner > #breaking-news { - grid-row: 1 / span 2; - grid-column: 1; -} - -#banner > #breaking-news-1 { - grid-row: 1; - grid-column: 2 / span 2; -} - -#banner > #breaking-news-2 { - grid-row: 2; - grid-column: 2; -} - -#banner > #breaking-news-3 { - grid-row: 2; - grid-column: 3; -} - -#content { - height: 80em; - background-style: solid; - background-color #f00; - - /* TODO */ -} - -#sidebar { - /* TODO */ -} - -/* css selector explanation - * -- - * main.grid > #content { ... } - * -- - * - given an element of type `main` with the class `grid` - * - for its direct child with id `content` - */ - -main.grid > #banner { - grid-row: 1; - grid-column: 1 / -1; -} - -/* on mobile ... */ -@media screen and (max-width:576px) { - /* the sidebar moves to immediately under the navbar */ - main.grid > #sidebar { - grid-row: 2 / span 1; - grid-column: 1 / span 2; - } - - /* the content comes after the "sidebar" */ - main.grid > #content { - grid-row: 3; - grid-column: 1 / span 2; - } -} - -/* on desktop ... */ -@media screen and (min-width:577px) { - /* the sidebar sits on the right-hand column (25% width) */ - main.grid > #sidebar { - grid-row: 2 / span 1; - grid-column: 2 / span 1; - } - - /* the contents sit on the left-hand column (75% width) */ - main.grid > #content { - grid-row: 2 / span 2; - grid-column: 1 / span 1; - } -} diff --git a/logo.svg b/logo.svg @@ -1,8 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> - <path d="M0 0 L512 0 L512 512 L0 512 L0 0" fill="rgb(0,0,0)" /> - <path d="M240 496 L80 336 L192 224 L192 16 L240 16 L240 240 L144 336 L240 432 L240 496" fill="rgb(255,255,255)" /> - <path d="M256 400 L192 336 L256 272 L320 336 L256 400" fill="rgb(255,165,0)" /> - <path d="M272 496 L432 336 L320 224 L320 16 L272 16 L272 240 L368 336 L272 432 L272 496" fill="rgb(255,255,255)" /> -</svg> - diff --git a/navbar.css b/navbar.css @@ -1,58 +0,0 @@ -.navbar { - padding-left: 1em; - padding-right: 1em; -} - -.navbar-menu { - padding: 0; - margin: 0; - list-style: none; -} - -.navbar-menu li { - display: inline-block; -} - -.navbar-toggle-label { - cursor: pointer; -} - -/* css selector explanation - * -- - * .navbar-toggle ~ .navbar-menu li { ... } - * -- - * - given an element with the `navbar-toggle` class, - * - for all subsequent siblings with the `navbar-menu` class, - * - for all their children of type <li> - */ - -/* on mobile ... */ -@media screen and (max-width:576px) { - /* always display the hamburger menu */ - .navbar-toggle ~ .navbar-menu li:first-child { - display: inline-block; - } - - /* never display the remaining list items ... */ - .navbar-toggle ~ .navbar-menu li { - display: none; - } - - /* ... unless the hamburger menu button has been clicked */ - .navbar-toggle:checked ~ .navbar-menu li { - display: block; - } -} - -/* on desktop ... */ -@media screen and (min-width:577px) { - /* never display the hamburger menu */ - .navbar-menu li:first-child { - display: none; - } - - /* always display the remaining list items */ - .navbar-menu li { - display: inline-block; - } -} diff --git a/src/css/article.css b/src/css/article.css @@ -0,0 +1,8 @@ +.articles { + list-style: none; + padding: 0; +} + +.article { + background-color: mintcream; +} diff --git a/src/css/layout.css b/src/css/layout.css @@ -0,0 +1,112 @@ +main.grid { + grid-template-rows: minmax(8em, 12em) auto auto; /* TODO */ + grid-template-columns: 3fr 1fr; +} + +#banner { + /* TODO */ +} + +#banner.grid { + grid-template-rows: 1fr 1fr; + grid-template-columns: 3fr 1fr 1fr; + + background-color: lightgrey; +} + +#banner > #breaking-news { + grid-row: 1 / span 2; + grid-column: 1; + + background-color: red; +} + +#banner > #breaking-news-1 { + grid-row: 1; + grid-column: 2 / span 2; + + background-color: magenta; +} + +#banner > #breaking-news-2 { + grid-row: 2; + grid-column: 2; + + background-color: blue; +} + +#banner > #breaking-news-3 { + grid-row: 2; + grid-column: 3; + + background-color: yellow; +} + +#content { + height: 80em; + background-color: grey; + + /* TODO */ +} + +#sidebar { + background-color: cyan; + + /* TODO */ +} + +/* css selector explanation + * -- + * main.grid > #content { ... } + * -- + * - given an element of type `main` with the class `grid` + * - for its direct child with id `content` + */ + +main.grid > #banner { + grid-row: 1 / span 1; + grid-column: 1 / -1; +} + +/* on mobile ... */ +@media screen and (max-width:576px) { + /* the sidebar moves to immediately under the banner */ + main.grid > #sidebar { + grid-row: 2 / span 1; + grid-column: 1 / span 2; + } + + /* the content comes after the "sidebar" */ + main.grid > #content { + grid-row: 3; + grid-column: 1 / span 2; + } +} + +/* on smaller screens ... */ +@media screen and (max-width:949px) { + /* the top banner has one less tile (for readability) ... */ + #banner > #breaking-news-2 { + grid-column: 2 / span 2; + } + + /* final tile is not displayed */ + #banner > #breaking-news-3 { + display: none; + } +} + +/* on desktop ... */ +@media screen and (min-width:577px) { + /* the sidebar sits on the right-hand column (25% width) */ + main.grid > #sidebar { + grid-row: 2 / span 1; + grid-column: 2 / span 1; + } + + /* the contents sit on the left-hand column (75% width) */ + main.grid > #content { + grid-row: 2 / span 2; + grid-column: 1 / span 1; + } +} diff --git a/src/css/navbar.css b/src/css/navbar.css @@ -0,0 +1,92 @@ +.navbar { +} + +.navbar-menu { + margin-top: 0; + margin-bottom: 0; + list-style: none; +} + +.navbar-menu li { + display: inline-block; +} + +.navbar-toggle-label { + cursor: pointer; +} + +/* css selector explanation + * -- + * .navbar-toggle ~ .navbar-menu li { ... } + * -- + * - given an element with the `navbar-toggle` class, + * - for all subsequent siblings with the `navbar-menu` class, + * - for all their children of type <li> + * + * -- + * .navbar-toggle:checked + .navbar-menu { ... } + * -- + * - given an element with the `navbar-toggle` class, that is currently checked + * - if its immediate (first next) sibling has the `navbar-menu` class + */ + +/* on mobile ... */ +@media screen and (max-width:576px) { + /* always display the hamburger menu button */ + .navbar-toggle-label { + display: inline-block; + } + + /* dont pad navbar menu ... */ + .navbar-menu { + padding: 0; + } + + /* ... unless the hamburger menu button has been clicked */ + .navbar-toggle:checked + .navbar-menu { + padding: 1em; + } + + /* never display the remaining list items ... */ + .navbar-toggle ~ .navbar-menu li { + display: none; + } + + /* ... unless the hamburger menu button has been clicked */ + .navbar-toggle:checked ~ .navbar-menu li { + display: block; + } + + /* never display the bottom navbar border ... */ + .navbar-divider { + display: none; + } + + /* ... unless the hamburger menu buttong has been clicked */ + .navbar-toggle:checked ~ .navbar-divider { + display: block; + } +} + +/* on desktop ... */ +@media screen and (min-width:577px) { + /* never display the hamburger menu */ + .navbar-toggle-label { + display: none; + } + + /* always have 1em of padding around the navbar items */ + .navbar-menu { + padding: 1em; + } + + /* always display the remaining list items */ + .navbar-menu li { + display: inline-block; + } + + /* always display the bottom navbar border */ + .navbar-divider { + display: block; + } +} diff --git a/src/css/style.css b/src/css/style.css @@ -0,0 +1,48 @@ +body, hr, html { + height: 100%; + margin: 0!important; + padding: 0!important; +} + +hr { + display: block; + height: 1px; + border: 0; + border-top: 1px solid; + border-color: inherit; +} + +.center { + max-width: 80em; + margin-left: auto; + margin-right: auto; +} + +.hidden { + display: none; +} + +.flex { + display: flex; + justify-content: space-between; +} + +.grid { + display: grid; +} + +.pad-l-1em { + padding-left: 1em; +} + +.pad-r-1em { + padding-right: 1em; +} + +.pad-t-1em { + padding-top: 1em; +} + +.pad-b-1em { + padding-bottom: 1em; +} diff --git a/src/html/science/article.html b/src/html/science/article.html @@ -0,0 +1,16 @@ +title: Article +tags: + +created-iso: 2026-04-10T21:18:37 +edited-iso: 2026-04-10T21:18:37 + +snippet: Lorem Ipsum Dolor Sit Amet. +caption: My figure caption + +img-cap: my image +img-src: /res/img.png +img-alt: my image alt +--- +<p> + Article Body. +</p> diff --git a/src/html/society/article.html b/src/html/society/article.html @@ -0,0 +1,16 @@ +title: Article +tags: + +created-iso: 2026-04-13T21:18:37 +edited-iso: 2026-04-13T21:18:37 + +snippet: Lorem Ipsum Dolor Sit Amet. +caption: My figure caption + +img-cap: my image +img-src: /res/img.png +img-alt: my image alt +--- +<p> + Article Body. +</p> diff --git a/src/html/sport/article.html b/src/html/sport/article.html @@ -0,0 +1,16 @@ +title: Article +tags: + +created-iso: 2026-04-12T21:18:37 +edited-iso: 2026-04-12T21:18:37 + +snippet: Lorem Ipsum Dolor Sit Amet. +caption: My figure caption + +img-cap: my image +img-src: /res/img.png +img-alt: my image alt +--- +<p> + Article Body. +</p> diff --git a/script.js b/src/js/script.js diff --git a/menu.svg b/src/res/menu.svg diff --git a/style.css b/style.css @@ -1,37 +0,0 @@ -body, hr, html { - height: 100%; - margin: 0!important; - padding: 0!important; -} - -hr { - display: block; - height: 1px; - border: 0; - border-top: 1px solid; - border-color: inherit; -} - -.center { - max-width: 80em; - margin-left: auto; - margin-right: auto; - /* padding: 1em; */ -} - -.pad-1em { - padding: 1em; -} - -.hidden { - display: none; -} - -.flex { - display: flex; - justify-content: space-between; -} - -.grid { - display: grid; -} diff --git a/templates/article.html b/templates/article.html @@ -0,0 +1,27 @@ +<article class="article"> + <figure class="article-img"> + <img title="%img-cap%" src="%img-src%" alt="%img-alt%" /> + <figcaption>%caption%</figcaption> + </figure> + <dl class="article-info"> + <dt class="article-info-meta">Author:</dt> + <dd title="Author"> + <span>%author%</span> + </dd> + <dt class="article-info-meta">Posted:</dt> + <dd title="Posted"> + <time id="post-date" datetime="%created-iso%">%created-str%</time> + </dd> + <dt class="article-info-meta">Edited:</dt> + <dd title="Edited"> + <time id="edit-date" datetime="%edited-iso%">%edited-str%</time> + </dd> + </dl> + <div class="article-text"> + <h2><a href="%url%">%title%</a></h2> + <p>%snippet%</p> + <div> +%content% + </div> + </div> +</article> diff --git a/templates/global.html b/templates/global.html @@ -0,0 +1,79 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <title>%title%</title> + + <meta charset="utf8" /> + <meta name="viewport" content="initial-scale=1.0, width=device-width" /> + + <link rel="stylesheet" href="/css/style.css" /> + <link rel="stylesheet" href="/css/navbar.css" /> + <link rel="stylesheet" href="/css/layout.css" /> + <link rel="stylesheet" href="/css/article.css" /> + </head> + <body> + <header> + <div class="flex center pad-t-1em pad-b-1em"> + <img style="max-width: 2em" src="/res/logo.svg" /> + <label class="navbar-toggle-label" for="global-menu-toggle"> + <img src="/res/menu.svg" /> + </label> + </div> + <hr /> + <nav id="global-menu" class="navbar"> + <input id="global-menu-toggle" type="checkbox" class="hidden navbar-toggle" /> + <ul class="center navbar-menu"> + <li><a href="/index.html">Index</a></li> + <li><a href="/science.html">Science</a></li> + <li><a href="/society.html">Society</a></li> + <li><a href="/sport.html">Sport</a></li> + </ul> + <hr class="navbar-divider" /> + </nav> + </header> + <main class="grid center"> + <div id="banner" class="grid"> + <div id="breaking-news"> + One + </div> + <div id="breaking-news-1"> + Two + </div> + <div id="breaking-news-2"> + Three + </div> + <div id="breaking-news-3"> + Four + </div> + </div> + <div id="content"> +%content% + </div> + <div id="sidebar"> + <section id="recent"> + <h3>Recent</h3> + <ol> + <li><a href="recent1.html">Recent 1</a></li> + <li><a href="recent2.html">Recent 2</a></li> + <li><a href="recent3.html">Recent 3</a></li> + </ol> + </section> + <section id="polls"> + <h3>Polls</h3> + <ol> + <li><a href="poll1.html">Poll 1</a></li> + <li><a href="poll2.html">Poll 2</a></li> + <li><a href="poll3.html">Poll 3</a></li> + </ol> + </section> + </div> + </main> + <footer> + <hr /> + <div class="center pad-t-1em pad-b-1em"> + <span>Lektura &copy; 2026</span> + </div> + </footer> + </body> + <script src="/js/script.js"></script> +</html> diff --git a/templates/index.html b/templates/index.html @@ -0,0 +1,5 @@ +<section id="content-body"> + <ul class="articles"> +%content% + </ul> +</section> diff --git a/templates/listed-article.html b/templates/listed-article.html @@ -0,0 +1,24 @@ +<article class="article"> + <figure class="article-img"> + <img title="%img-cap%" src="%img-src%" alt="%img-alt%" /> + <figcaption>%caption%</figcaption> + </figure> + <dl class="article-info"> + <dt class="article-info-meta">Author:</dt> + <dd title="Author"> + <span>%author%</span> + </dd> + <dt class="article-info-meta">Posted:</dt> + <dd title="Posted"> + <time id="post-date" datetime="%created-iso%">%created-str%</time> + </dd> + <dt class="article-info-meta">Edited:</dt> + <dd title="Edited"> + <time id="edit-date" datetime="%edited-iso%">%edited-str%</time> + </dd> + </dl> + <div class="article-text"> + <h2><a href="%url%">%title%</a></h2> + <p>%snippet%</p> + </div> +</article> diff --git a/templates/listitem.html b/templates/listitem.html @@ -0,0 +1 @@ +%content% diff --git a/tools/convert.py b/tools/convert.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 + +import argparse + +argparser = argparse.ArgumentParser( + description='Converts markdown source files into html fragments') + +argparser.add_argument(['-s', '--src'], 'src', required=True) +argparser.add_argument(['-d', '--dst'], 'dst', required=True) + +if __name__ == '__main__': + args = argparser.parse_args() + + print(args) + + diff --git a/tools/index.py b/tools/index.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 + +import argparse +import html.parser +import os +import pathlib + +import template + + +def dirpath(path): + if os.path.isdir(path): + return path + + raise argparse.ArgumentError() + + +argparser = argparse.ArgumentParser( + description='Indexes a given set of html fragments') + +argparser.add_argument('dst', type=argparse.FileType('w'), + help='The destination html fragment') +argparser.add_argument('art_tpl', metavar='element-tpl', type=argparse.FileType('r'), + help='The html fragment template for each list element') +argparser.add_argument('idx_tpl', metavar='index-tpl', type=argparse.FileType('r'), + help='The html fragment template for the index page') +argparser.add_argument('urlroot', type=str, + help='The base url for each article (e.g. "/sport")') +argparser.add_argument('srcs', nargs='*', + help='A list of html fragments to index') + + +class TagParser(html.parser.HTMLParser): + def __init__(self, **kwargs): + super().__init__() + + self.filter_tag = kwargs.get('tag', None) + self.filter_id = kwargs.get('id', None) + self.found_data = '' + self.found_attrs = [] + + self.have_tag = False + self.have_data = False + + @staticmethod + def get_attr(key, attrs, default): + for attr in attrs: + if attr[0] == key: + return attr[1] + + return default + + def handle_starttag(self, tag, attrs): + if self.have_tag: + return + + if self.filter_tag and self.filter_tag != tag: + return + + if self.filter_id and self.filter_id != self.get_attr('id', attrs, ''): + return + + self.have_tag = True + self.found_attrs = attrs + + def handle_startendtag(self, tag, attrs): + self.handle_starttag(tag, attrs) + + def handle_data(self, data): + if not self.have_tag: + return + + if self.have_tag and self.have_data: + return + + self.have_data = True + self.found_data = data + + + +def index(srcs, article_tpl, index_tpl, urlroot): + content = '' + + def by_post_date(srcpath): + # NOTE: this parser expects at least one element of type `<time>` and + # with an id of `post-date` to be present in the given article. + # if this requirement is not satisfied, it throws an error + parser = TagParser(tag='time', id='post-date') + content = template.content(srcpath) + parser.feed(content) + + if not parser.have_tag or not parser.have_data: + raise Exception(f'Article: {srcpath} lacks element <time id="post-date" ... />') + + isodate = TagParser.get_attr('datetime', parser.found_attrs, None) + print(isodate) + + return template.fromisodate(isodate) + + for article in sorted(srcs, key=by_post_date, reverse=True): + srcpath = pathlib.Path(article) + + keys = template.article_keys(srcpath) + keys |= { + 'url': f'{urlroot}/{srcpath.name}', + } + + content += template.instantiate(article_tpl, keys) + + keys = { 'content': content } + + return template.instantiate(index_tpl, keys) + + +if __name__ == '__main__': + args = argparser.parse_args() + + article_tpl = '' + with args.art_tpl as tpl: + article_tpl = tpl.read() + + index_tpl = '' + with args.idx_tpl as tpl: + index_tpl = tpl.read() + + with args.dst as dst: + dst.write(index(args.srcs, article_tpl, index_tpl, args.urlroot)) + diff --git a/tools/template.py b/tools/template.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 + +import argparse +import datetime +import html +import html.parser +import os +import pathlib + + +argparser = argparse.ArgumentParser( + description='Templates html fragments into full html pages') + +argparser.add_argument('src', type=argparse.FileType('r'), + help='The source html fragment') +argparser.add_argument('dst', type=argparse.FileType('w'), + help='The destination html file') +argparser.add_argument('tpl', type=argparse.FileType('r'), + help='The source html template') +argparser.add_argument('kwargs', nargs='*', + help='A list of custom \'key=val\' pairs to replace in the template') + + +def map_kwargs(kwargs): + return dict([arg.split('=', maxsplit=1) for arg in kwargs]) + + +def titleify(srcpath): + slug, ext = srcpath.rsplit('.', maxsplit=1) + + seps = ['-', '_'] + for sep in seps: + slug = slug.replace(sep, ' ') + + frags = [substr.capitalize() for substr in slug.split(' ')] + title = ' '.join(frags) + + return title + + +def created(srcpath): + info = os.stat(srcpath) + return datetime.datetime.fromtimestamp(info.st_ctime) + + +def edited(srcpath): + info = os.stat(srcpath) + return datetime.datetime.fromtimestamp(info.st_mtime) + + +def fromisodate(isodate): + return datetime.datetime.fromisoformat(isodate) + + +TIMESTAMP = '%d %B %Y' + +METADATA_SEPARATOR = '---' + + +def readfile(srcpath): + data = '' + with open(srcpath, 'r') as src: + data = src.read() + + if METADATA_SEPARATOR in data: + return data.split(METADATA_SEPARATOR, maxsplit=1) + + return '', data + + +def metadata(srcpath): + [metadata, *_] = readfile(srcpath) + + keys = {} + for line in metadata.split('\n'): + line = line.strip() + if line: + [key, *val] = line.split(':', maxsplit=1) + keys[key] = ''.join(val).strip() + + return keys + + +def content(srcpath): + [_, *contents] = readfile(srcpath) + + return ' '.join(contents) + + +def article_keys(srcpath): + name = pathlib.Path(srcpath).name + + keys = { + 'title': titleify(name), + 'created-iso': created(srcpath).isoformat(), + 'edited-iso': edited(srcpath).isoformat(), + 'content': content(srcpath), + } + + keys |= metadata(srcpath) + + keys |= { + 'created-str': fromisodate(keys['created-iso']).strftime(TIMESTAMP), + 'edited-str': fromisodate(keys['edited-iso']).strftime(TIMESTAMP), + } + + # print(srcpath, keys) + + return keys + + +def instantiate(tpl, kwargs): + splits = [] + + cur = 0 + end = len(tpl) + while cur < end: + key_start = tpl.find('%', cur) + key_end = tpl.find('%', key_start + 1) + + if key_start == -1: + splits.append([cur, end, '']) + break + else: + splits.append([cur, key_start, tpl[key_start + 1:key_end]]) + + cur = key_end + 1 + + out = '' + for [tpl_start, tpl_end, key] in splits: + out += tpl[tpl_start:tpl_end] + out += kwargs.get(key, '') + + return out + + +if __name__ == '__main__': + args = argparser.parse_args() + + template = '' + with args.tpl as tpl: + template = tpl.read() + + with args.dst as dst: + keys = article_keys(args.src.name) + keys |= map_kwargs(args.kwargs) + + dst.write(instantiate(template, keys)) + diff --git a/tools/utils.py b/tools/utils.py @@ -0,0 +1,3 @@ +""" +utils.py: helper functions +"""