Browse Source

start to move generator in-repo

Getty Ritter 1 year ago
parent
commit
1dcce8bcc0

+ 203 - 0
main.py

@@ -0,0 +1,203 @@
+#!/usr/bin/env python3
+
+from dataclasses import dataclass
+import datetime
+import os
+import markdown
+import pystache
+import shutil
+import yaml
+
+class Datum:
+    @classmethod
+    def from_yaml(cls, data):
+        return cls(**data)
+
+    @classmethod
+    def from_file(cls, path):
+        with open(path) as f:
+            data = yaml.safe_load(f)
+            return cls.from_yaml(data)
+
+@dataclass
+class Quote(Datum):
+    id: str
+    content: str
+    author: str
+
+@dataclass
+class Quip(Datum):
+    id: str
+    content: str
+
+@dataclass
+class Work:
+    slug: str
+    category: str
+    title: str
+    date: str
+    contents: str
+
+class Path:
+    DATADIR=os.getenv('DATADIR', '/home/gdritter/projects/lib-data')
+    OUTDIR=os.getenv('OUTDIR', '/tmp/lib')
+
+    @classmethod
+    def data(cls, *paths):
+        return os.path.join(cls.DATADIR, *paths)
+
+    @classmethod
+    def out(cls, *paths):
+        return os.path.join(cls.OUTDIR, *paths)
+
+    @classmethod
+    def write(cls, *paths):
+        if len(paths) > 1:
+            os.makedirs(cls.out(*paths[:-1]), exist_ok=True)
+        return open(cls.out(*paths), 'w')
+
+    @classmethod
+    def read(cls, *paths):
+        with open(cls.data(*paths)) as f:
+            return f.read()
+
+    @classmethod
+    def list(cls, *paths):
+        return os.listdir(cls.data(*paths))
+
+class Template:
+    renderer = pystache.Renderer(search_dirs="templates")
+
+    def load_template(name):
+        with open(f"templates/{name}.mustache") as f:
+            parsed = pystache.parse(f.read())
+        return lambda stuff: Template.renderer.render(parsed, stuff)
+
+    main = load_template("main")
+    quote = load_template("quote")
+    list = load_template("list")
+
+
+def main():
+    year = datetime.datetime.now().year
+    std_copy = f'© Getty Ritter {year}'
+    no_copy = 'all rights reversed'
+
+    # gather the quips and make their individual pages
+    quips = []
+    for uuid in Path.list('quips'):
+        q = Quip.from_file(Path.data('quips', uuid))
+        quips.append(q)
+        with Path.write('quips', uuid, 'index.html') as f:
+            f.write(Template.main({
+                'title': f"Quip {uuid}",
+                'contents': Template.quote({'quotelist': [q]}),
+                'copy': no_copy,
+            }))
+
+    # sort 'em and make the combined page
+    quips.sort(key=lambda q: q.id)
+    with Path.write('quips', 'index.html') as f:
+        f.write(Template.main({
+            'title': "Quips",
+            'contents': Template.quote({'quotelist': quips}),
+            'copy': no_copy,
+        }))
+
+    # gather the quotes and make their individual pages
+    quotes = []
+    for uuid in Path.list('quotes'):
+        q = Quote.from_file(Path.data('quotes', uuid))
+        quotes.append(q)
+        with Path.write('quotes', uuid, 'index.html') as f:
+            f.write(Template.main({
+                'title': f"Quote {uuid}",
+                'contents': Template.quote({'quotelist': [q]}),
+                'copy': no_copy,
+            }))
+
+    # sort 'em and make their combined page
+    quotes.sort(key=lambda q: q.id)
+    with Path.write('quotes', 'index.html') as f:
+        f.write(Template.main({
+            'title': "Quotes",
+            'contents': Template.quote({'quotelist': quotes}),
+            'copy': no_copy,
+        }))
+
+    # figure out what categories we've got
+    with open(Path.data('works.json')) as f:
+        categories = yaml.safe_load(f)
+
+    # make an index page for each category
+    with Path.write('category', 'index.html') as f:
+        f.write(Template.main({
+            'title': 'Categories',
+            'contents': Template.list({
+                'works': [
+                    {'slug': f'category/{c["slug"]}', 'title': c['category']}
+                    for c in categories
+                ]
+            }),
+            'copy': 'whatever',
+        }))
+
+    # create each category page
+    for slug in os.listdir(Path.data('works')):
+        # we need to know what works exist in the category
+        works = []
+        for work in os.listdir(Path.data('works', slug)):
+            # grab the metadata for this work
+            with open(Path.data('works', slug, work, 'metadata.yaml')) as f:
+                meta = yaml.safe_load(f)
+            with open(Path.data('works', slug, work, 'text')) as f:
+                text = markdown.markdown(f.read())
+            w = Work(
+                slug=meta.get('slug', work),
+                category=meta.get('category', slug),
+                title=meta['name'],
+                date=meta['date'],
+                contents=text,
+            )
+
+            if slug == 'pages':
+                # always keep index/about up-to-date
+                copy = std_copy
+            else:
+                # report other works in their own year
+                copy = f'© Getty Ritter {w.date}'
+
+            with Path.write(w.slug, 'index.html') as f:
+                f.write(Template.main({
+                    'title': w.title,
+                    'contents': text,
+                    'copy': copy,
+                }))
+            works.append(w)
+        works.sort(key=lambda w: w.slug)
+
+        # not every on-disk category should be shown: we should find
+        # it in the categories list first
+        category_metadata = [c for c in categories if c['slug'] == slug]
+        if not category_metadata:
+            print(f'skipping {slug}')
+            continue
+
+        with Path.write('category', slug, 'index.html') as f:
+            f.write(Template.main({
+                'title': category_metadata[0]['category'],
+                'contents': Template.list({
+                    'works': works,
+                }),
+                'copy': std_copy,
+            }))
+
+    shutil.copy(Path.out('index', 'index.html'), Path.out('index.html'))
+
+    os.makedirs(Path.out('static'), exist_ok=True)
+    shutil.copy('static/main.css', Path.out('static', 'main.css'))
+    shutil.copy('static/icon.png', Path.out('static', 'icon.png'))
+
+
+if __name__ == '__main__':
+    main()

+ 3 - 0
requirements.txt

@@ -0,0 +1,3 @@
+Markdown==3.3.6
+pystache==0.6.0
+PyYAML==6.0

BIN
static/icon.png


+ 115 - 0
static/main.css

@@ -0,0 +1,115 @@
+@font-face {
+    font-family: league-spartan;
+    src: url("/static/leaguespartan-bold.ttf");
+}
+
+body {
+	background-color: #eeeeee;
+	font-family: league-spartan, sans-serif;
+	font-size: 14pt;
+}
+
+.header {
+	padding: 20px;
+	text-transform: uppercase;
+}
+
+.sitename {
+	margin: 0px;
+	font-size: 24pt;
+	letter-spacing: 4px;
+	padding-left: 40px;
+	color: #ccc;
+	padding-right: 80px;
+	margin-top: -40px;
+	text-align: right;
+}
+
+.title {
+	margin-left: 40px;
+	margin-top: -40px;
+	letter-spacing: 4px;
+}
+
+.contents {
+	text-align: justify;
+	line-height: 1.5;
+	padding: 40px;
+	font-family: "Helvetica", "Arial", sans-serif;
+	margin: 40px;
+	width: 80%;
+	background-color: #e8e8e8;
+	margin-left: auto;
+	margin-right: auto;
+}
+
+.menu {
+	text-transform: uppercase;
+	letter-spacing: 2px;
+	text-align: center;
+	border-top-style: solid;
+	border-bottom-style: solid;
+	padding-top: 5px;
+	padding-bottom: 5px;
+	border-width: 1px;
+	width: 60%;
+	margin-left: auto;
+	margin-right: auto;
+}
+
+a:link {
+	text-decoration: none;
+	color: #cc3366;
+}
+
+a:visited {
+	color: #993366;
+}
+
+.footer {
+	text-align: center;
+	letter-spacing: 4px;
+	text-transform: uppercase;
+}
+
+pre {
+    text-indent: 0px;
+    padding-left: 20px;
+}
+
+.quote {
+	border-top-style: solid;
+	border-bottom-style: solid;
+	border-width: 1px;
+	padding: 20px;
+	margin-top: 20px;
+	margin-bottom: 20px;
+	width: 75%;
+	margin-left: auto;
+	margin-right: auto;
+	background-color: #eee;
+}
+.quotelink {
+	margin-top: 10px;
+	text-align: right;
+	font-size: 12pt;
+}
+
+.link {
+	border-top-style: solid;
+	border-bottom-style: solid;
+	border-width: 1px;
+	padding: 20px;
+	margin-top: 20px;
+	margin-bottom: 20px;
+	width: 75%;
+	margin-left: auto;
+	margin-right: auto;
+	background-color: #eee;
+}
+.permalink {
+	margin-top: 10px;
+	text-align: right;
+	font-size: 12pt;
+}
+

+ 11 - 0
templates/link.mustache

@@ -0,0 +1,11 @@
+{{! linklist }}
+<div class="links">
+  {{#linklist}}
+  <div class="link" id="{{id}}">
+      <div class="name"><a href="{{url}}">{{name}}{{^name}}{{url}}{{/name}}</a></div>
+      {{#description}}<div class="description">{{description}}</div>{{/description}}
+      {{#name}}<div class="url"><pre><a href="{{url}}">{{url}}</a></pre></div>{{/name}}
+      <div class="permalink"><a href="#{{id}}" onClick="doHighlight('{{id}}')">#{{id}}</a></div>
+  </div>
+  {{/linklist}}
+</div>

+ 11 - 0
templates/list.mustache

@@ -0,0 +1,11 @@
+{{! (pgnum, maxpg, works, show_cat, page='recent') }}
+<div class="maintext">
+  {{#works}}
+    <div class="work">
+      <a href="/{{slug}}/">{{title}}</a>
+      {{#showcat}}
+        &mdash; <a href="/category/{{category}}/">{{category}}</a>
+      {{/showcat}}
+    </div>
+  {{/works}}
+</div>

+ 46 - 0
templates/main.mustache

@@ -0,0 +1,46 @@
+{{! title, contents, usejs=False, onLoad='', copy='&copy;2022 Getty Ritter' }}
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+      "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+  <head>
+    <meta http-equiv="Content-Type" content="application/xhtml+xml; charset=utf-8;" />
+    {{#opengraph}}
+      <meta property="og:title" content="{{title}}"/>
+      <meta property="og:type" content="website"/>
+      <meta property="og:url" content="{{url}}"/>
+      <meta property="og:description" content="{{description}}"/>
+      <meta property="og:image" content="/static/icon.png"/>
+      <meta property="og:image:width" content="120"/>
+      <meta property="og:image:height" content="120"/>
+      <meta property="og:site_name" content="Librarian of Alexandria"/>
+    {{/opengraph}}
+    <link rel="stylesheet" type="text/css" href="/static/main.css" />
+    <title>Librarian of Alexandria: {{title}}</title>
+  </head>
+  <body>
+    <div class="all">
+      <div class="header">
+    <div class="sitename">
+      <h1>Librarian of Alexandria</h1>
+    </div>
+    <div class="title">
+      <h2>{{title}}</h2>
+    </div>
+      </div>
+      <div class="menu">
+    <a href="/">Index</a>&nbsp;&nbsp;&nbsp;
+    <a href="/category/">Collections</a>&nbsp;&nbsp;&nbsp;
+    <a href="/quotes/">Quotes</a>&nbsp;&nbsp;&nbsp;
+    <a href="/quips/">Quips</a>&nbsp;&nbsp;&nbsp;
+    <a href="http://journal.infinitenegativeutility.com/">Blog</a>&nbsp;&nbsp;&nbsp;
+    <a href="/about/">About</a>
+      </div>
+      <div class="contents">
+    {{{contents}}}
+      </div>
+      <div class="footer">
+    {{copy}}{{^copy}}&copy;2022 Getty Ritter{{/copy}}
+      </div>
+    </div>
+  </body>
+</html>

+ 13 - 0
templates/quote.mustache

@@ -0,0 +1,13 @@
+<div class="quotes">
+  {{#quotelist}}
+    <div class="quote" id="{{id}}">
+      {{{content}}}
+    {{#author}}
+      <div class="author">
+        &mdash;{{{author}}}
+      </div>
+    {{/author}}
+    <div class="quotelink"><a href="{{id}}">#{{id}}</a></div>
+    </div>
+  {{/quotelist}}
+</div>

+ 15 - 0
templates/scrap.mustache

@@ -0,0 +1,15 @@
+<div class="scraps">
+  {{#scraplist}}
+    <div class="scrap" id="{{id}}">
+      {{{content}}}
+    <div class="related">
+      {{#related}}
+        <a href=/scraps/{{name}}><code>{{name}}</code>: {{why}}</a><br/>
+      {{/related}}
+    </div>
+    {{^focus}}
+      <div class="scraplink"><a href="{{id}}" onClick="doHighlight('{{id}}')">#{{id}}</a></div>
+    {{/focus}}
+    </div>
+  {{/scraplist}}
+</div>

+ 3 - 0
templates/textpage.mustache

@@ -0,0 +1,3 @@
+<div class="maintext">
+  {{{contents}}}
+</div>