main.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. #!/usr/bin/env python3
  2. from dataclasses import dataclass
  3. import base58
  4. import datetime
  5. import os
  6. import markdown
  7. import pystache
  8. import shutil
  9. import sys
  10. import tempfile
  11. from typing import Optional
  12. import yaml
  13. def short_hash(s: str) -> str:
  14. return base58.b58encode(bytes.fromhex(s.replace('-', '')))[:16].decode('utf-8')
  15. class Datum:
  16. @classmethod
  17. def from_yaml(cls, data):
  18. return cls(**data)
  19. @classmethod
  20. def from_file(cls, path):
  21. with open(path) as f:
  22. data = yaml.safe_load(f)
  23. return cls.from_yaml(data)
  24. @dataclass
  25. class Quote(Datum):
  26. id: str
  27. content: str
  28. author: str
  29. def short_hash(self) -> str:
  30. return "qt_" + short_hash(self.id)
  31. @dataclass
  32. class Quip(Datum):
  33. id: str
  34. content: str
  35. def short_hash(self) -> str:
  36. return "qp_" + short_hash(self.id)
  37. @dataclass
  38. class Work:
  39. slug: str
  40. category: str
  41. title: str
  42. date: str
  43. contents: str
  44. description: Optional[str] = None
  45. class Path:
  46. OUTDIR=tempfile.TemporaryDirectory()
  47. @classmethod
  48. def data(cls, *paths):
  49. tgt = os.path.join(*paths)
  50. for path in sys.argv[2:]:
  51. if path.endswith(os.path.join(tgt)):
  52. return path
  53. raise Exception(f"Could not find {tgt}")
  54. @classmethod
  55. def out(cls, *paths):
  56. return os.path.join(cls.OUTDIR.name, *paths)
  57. @classmethod
  58. def write(cls, *paths):
  59. if len(paths) > 1:
  60. os.makedirs(cls.out(*paths[:-1]), exist_ok=True)
  61. return open(cls.out(*paths), 'w')
  62. @classmethod
  63. def read(cls, *paths):
  64. with open(cls.data(*paths)) as f:
  65. return f.read()
  66. @classmethod
  67. def list(cls, *paths):
  68. stuff = set()
  69. tgt = f'{os.path.join(*paths)}/'
  70. for path in sys.argv[2:]:
  71. if tgt in path:
  72. chunks = path.split('/')
  73. idx = chunks.index(paths[-1])
  74. stuff.add(chunks[idx +1])
  75. return stuff
  76. class Template:
  77. renderer = pystache.Renderer(search_dirs="templates")
  78. def load_template(name):
  79. with open(f"templates/{name}.mustache") as f:
  80. parsed = pystache.parse(f.read())
  81. return lambda stuff: Template.renderer.render(parsed, stuff)
  82. main = load_template("main")
  83. quote = load_template("quote")
  84. list = load_template("list")
  85. textpage = load_template("textpage")
  86. def main():
  87. out_file = sys.argv[1]
  88. year = datetime.datetime.now().year
  89. std_copy = f'©{year} Getty Ritter'
  90. no_copy = 'all rights reversed'
  91. # gather the quips and make their individual pages
  92. quips = []
  93. for uuid in Path.list('quips'):
  94. q = Quip.from_file(Path.data('quips', uuid))
  95. q.content = markdown.markdown(q.content)
  96. quips.append(q)
  97. h = q.short_hash()
  98. html = Template.main({
  99. 'title': f"Quip",
  100. 'contents': Template.quote({'quotelist': [q]}),
  101. 'copy': no_copy,
  102. 'opengraph': {
  103. 'title': f'quip:{h}',
  104. 'url': f'/quip/{h}/',
  105. 'description': q.content,
  106. },
  107. })
  108. with Path.write('quips', uuid, 'index.html') as f:
  109. f.write(html)
  110. with Path.write('quips', h, 'index.html') as f:
  111. f.write(html)
  112. # sort 'em and make the combined page
  113. quips.sort(key=lambda q: q.id)
  114. with Path.write('quips', 'index.html') as f:
  115. f.write(Template.main({
  116. 'title': "Quips",
  117. 'contents': Template.quote({'quotelist': quips}),
  118. 'copy': no_copy,
  119. }))
  120. # gather the quotes and make their individual pages
  121. quotes = []
  122. for uuid in Path.list('quotes'):
  123. q = Quote.from_file(Path.data('quotes', uuid))
  124. q.content = markdown.markdown(q.content)
  125. q.author = markdown.markdown(q.author)
  126. quotes.append(q)
  127. contents = Template.quote({'quotelist': [q]})
  128. short_hash = q.short_hash()
  129. html = Template.main({
  130. 'title': f"Quote",
  131. 'contents': contents,
  132. 'copy': no_copy,
  133. 'opengraph': {
  134. 'title': f'quote:{short_hash}',
  135. 'url': f'/quote/{short_hash}/',
  136. 'description': f'{q.content}\n—{q.author}',
  137. },
  138. })
  139. with Path.write('quotes', uuid, 'index.html') as f:
  140. f.write(html)
  141. with Path.write('quotes', short_hash, 'index.html') as f:
  142. f.write(html)
  143. # sort 'em and make their combined page
  144. quotes.sort(key=lambda q: q.id)
  145. with Path.write('quotes', 'index.html') as f:
  146. f.write(Template.main({
  147. 'title': "Quotes",
  148. 'contents': Template.quote({'quotelist': quotes}),
  149. 'copy': no_copy,
  150. }))
  151. # figure out what categories we've got
  152. with open(Path.data('works.json')) as f:
  153. categories = yaml.safe_load(f)
  154. category_lookup = {c['slug']: c for c in categories}
  155. # make an index page for each category
  156. with Path.write('category', 'index.html') as f:
  157. f.write(Template.main({
  158. 'title': 'Categories',
  159. 'contents': Template.list({
  160. 'works': [
  161. {'slug': f'category/{c["slug"]}', 'title': c['category']}
  162. for c in categories
  163. ]
  164. }),
  165. 'copy': std_copy,
  166. }))
  167. # create each category page
  168. for slug in Path.list('works'):
  169. # we need to know what works exist in the category
  170. works = []
  171. for work in Path.list('works', slug):
  172. # grab the metadata for this work
  173. with open(Path.data('works', slug, work, 'metadata.yaml')) as f:
  174. meta = yaml.safe_load(f)
  175. with open(Path.data('works', slug, work, 'text')) as f:
  176. text = Template.textpage({'contents': markdown.markdown(f.read(), extensions=['footnotes'])})
  177. w = Work(
  178. slug=meta.get('slug', work),
  179. category=meta.get('category', slug),
  180. title=meta['name'],
  181. date=meta['date'],
  182. contents=text,
  183. )
  184. if slug == 'pages':
  185. # always keep index/about up-to-date
  186. copy = std_copy
  187. else:
  188. # report other works in their own year
  189. copy = f'© Getty Ritter {w.date}'
  190. if w.description is not None:
  191. description = w.description
  192. elif slug in category_lookup:
  193. singular = category_lookup[slug]['singular']
  194. description = f'{w.title}: a {singular}'
  195. else:
  196. description = '...'
  197. with Path.write(w.slug, 'index.html') as f:
  198. f.write(Template.main({
  199. 'title': w.title,
  200. 'contents': text,
  201. 'copy': copy,
  202. 'opengraph': {
  203. 'title': w.title,
  204. 'url': f'/{w.slug}/',
  205. 'description': description,
  206. },
  207. }))
  208. works.append(w)
  209. works.sort(key=lambda w: w.slug)
  210. # not every on-disk category should be shown: we should find
  211. # it in the categories list first
  212. category_metadata = [c for c in categories if c['slug'] == slug]
  213. if not category_metadata:
  214. continue
  215. with Path.write('category', slug, 'index.html') as f:
  216. f.write(Template.main({
  217. 'title': category_metadata[0]['category'],
  218. 'contents': Template.list({
  219. 'works': works,
  220. }),
  221. 'copy': std_copy,
  222. }))
  223. shutil.copy(Path.out('index', 'index.html'), Path.out('index.html'))
  224. os.makedirs(Path.out('static'), exist_ok=True)
  225. shutil.copy('static/main.css', Path.out('static', 'main.css'))
  226. shutil.copy('static/icon.png', Path.out('static', 'icon.png'))
  227. shutil.make_archive('output', 'zip', Path.OUTDIR.name)
  228. shutil.move('output.zip', out_file)
  229. Path.OUTDIR.cleanup()
  230. if __name__ == '__main__':
  231. main()