main.py 7.5 KB

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