main.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  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. def main():
  86. out_file = sys.argv[1]
  87. year = datetime.datetime.now().year
  88. std_copy = f'©{year} Getty Ritter'
  89. no_copy = 'all rights reversed'
  90. # gather the quips and make their individual pages
  91. quips = []
  92. for uuid in Path.list('quips'):
  93. q = Quip.from_file(Path.data('quips', uuid))
  94. q.content = markdown.markdown(q.content)
  95. quips.append(q)
  96. h = q.short_hash()
  97. html = Template.main({
  98. 'title': f"Quip",
  99. 'contents': Template.quote({'quotelist': [q]}),
  100. 'copy': no_copy,
  101. 'opengraph': {
  102. 'title': f'quip:{h}',
  103. 'url': f'/quip/{h}/',
  104. 'description': q.content,
  105. },
  106. })
  107. with Path.write('quips', uuid, 'index.html') as f:
  108. f.write(html)
  109. with Path.write('quips', h, 'index.html') as f:
  110. f.write(html)
  111. # sort 'em and make the combined page
  112. quips.sort(key=lambda q: q.id)
  113. with Path.write('quips', 'index.html') as f:
  114. f.write(Template.main({
  115. 'title': "Quips",
  116. 'contents': Template.quote({'quotelist': quips}),
  117. 'copy': no_copy,
  118. }))
  119. # gather the quotes and make their individual pages
  120. quotes = []
  121. for uuid in Path.list('quotes'):
  122. q = Quote.from_file(Path.data('quotes', uuid))
  123. q.content = markdown.markdown(q.content)
  124. q.author = markdown.markdown(q.author)
  125. quotes.append(q)
  126. contents = Template.quote({'quotelist': [q]})
  127. short_hash = q.short_hash()
  128. html = Template.main({
  129. 'title': f"Quote",
  130. 'contents': contents,
  131. 'copy': no_copy,
  132. 'opengraph': {
  133. 'title': f'quote:{short_hash}',
  134. 'url': f'/quote/{short_hash}/',
  135. 'description': f'{q.content}\n—{q.author}',
  136. },
  137. })
  138. with Path.write('quotes', uuid, 'index.html') as f:
  139. f.write(html)
  140. with Path.write('quotes', short_hash, 'index.html') as f:
  141. f.write(html)
  142. # sort 'em and make their combined page
  143. quotes.sort(key=lambda q: q.id)
  144. with Path.write('quotes', 'index.html') as f:
  145. f.write(Template.main({
  146. 'title': "Quotes",
  147. 'contents': Template.quote({'quotelist': quotes}),
  148. 'copy': no_copy,
  149. }))
  150. # figure out what categories we've got
  151. with open(Path.data('works.json')) as f:
  152. categories = yaml.safe_load(f)
  153. category_lookup = {c['slug']: c for c in categories}
  154. # make an index page for each category
  155. with Path.write('category', 'index.html') as f:
  156. f.write(Template.main({
  157. 'title': 'Categories',
  158. 'contents': Template.list({
  159. 'works': [
  160. {'slug': f'category/{c["slug"]}', 'title': c['category']}
  161. for c in categories
  162. ]
  163. }),
  164. 'copy': std_copy,
  165. }))
  166. # create each category page
  167. for slug in Path.list('works'):
  168. # we need to know what works exist in the category
  169. works = []
  170. for work in Path.list('works', slug):
  171. # grab the metadata for this work
  172. with open(Path.data('works', slug, work, 'metadata.yaml')) as f:
  173. meta = yaml.safe_load(f)
  174. with open(Path.data('works', slug, work, 'text')) as f:
  175. text = markdown.markdown(f.read(), extensions=['footnotes'])
  176. w = Work(
  177. slug=meta.get('slug', work),
  178. category=meta.get('category', slug),
  179. title=meta['name'],
  180. date=meta['date'],
  181. contents=text,
  182. )
  183. if slug == 'pages':
  184. # always keep index/about up-to-date
  185. copy = std_copy
  186. else:
  187. # report other works in their own year
  188. copy = f'© Getty Ritter {w.date}'
  189. if w.description is not None:
  190. description = w.description
  191. elif slug in category_lookup:
  192. singular = category_lookup[slug]['singular']
  193. description = f'{w.title}: a {singular}'
  194. else:
  195. description = '...'
  196. with Path.write(w.slug, 'index.html') as f:
  197. f.write(Template.main({
  198. 'title': w.title,
  199. 'contents': text,
  200. 'copy': copy,
  201. 'opengraph': {
  202. 'title': w.title,
  203. 'url': f'/{w.slug}/',
  204. 'description': description,
  205. },
  206. }))
  207. works.append(w)
  208. works.sort(key=lambda w: w.slug)
  209. # not every on-disk category should be shown: we should find
  210. # it in the categories list first
  211. category_metadata = [c for c in categories if c['slug'] == slug]
  212. if not category_metadata:
  213. continue
  214. with Path.write('category', slug, 'index.html') as f:
  215. f.write(Template.main({
  216. 'title': category_metadata[0]['category'],
  217. 'contents': Template.list({
  218. 'works': works,
  219. }),
  220. 'copy': std_copy,
  221. }))
  222. shutil.copy(Path.out('index', 'index.html'), Path.out('index.html'))
  223. os.makedirs(Path.out('static'), exist_ok=True)
  224. shutil.copy('static/main.css', Path.out('static', 'main.css'))
  225. shutil.copy('static/icon.png', Path.out('static', 'icon.png'))
  226. shutil.make_archive('output', 'zip', Path.OUTDIR.name)
  227. shutil.move('output.zip', out_file)
  228. Path.OUTDIR.cleanup()
  229. if __name__ == '__main__':
  230. main()