Personal homepage and static website generator for noah.is https://noah.is/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

build.py 4.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. #!/usr/bin/env python3
  2. import codecs
  3. import logging
  4. import os
  5. from shutil import copyfile
  6. import jinja2
  7. logging.basicConfig()
  8. log = logging.getLogger("build")
  9. log.setLevel(logging.INFO)
  10. class App(object):
  11. # Global template variables.
  12. def vars(self):
  13. return {
  14. "site": {
  15. "name": "noah.is",
  16. },
  17. }
  18. def __init__(self):
  19. # Jinja environment.
  20. self.env = jinja2.Environment(
  21. loader=jinja2.FileSystemLoader("./templates"),
  22. )
  23. # Collect all the templates and all the static files.
  24. self.templates = self.crawl("./templates") # TODO: remove list() wrapper
  25. self.static = self.crawl("./static")
  26. # Copy all of the static files into the public root.
  27. if not os.path.isdir("./public"):
  28. self.mkdir("./public")
  29. for static in self.static:
  30. public = static.replace("./static/", "./public/")
  31. self.mkdir(public.rsplit("/", 1)[0]) # ensure the path will exist
  32. if self.should_copy_static(static, public):
  33. log.info("COPY STATIC FILE %s -> %s", static, public)
  34. copyfile(static, public)
  35. # And render all of the templates.
  36. for template in self.templates:
  37. if template.startswith("./templates/layout"): # don't create these
  38. continue
  39. public = self.template_to_public(template)
  40. self.mkdir(public.rsplit("/", 1)[0]) # ensure the path will exist
  41. log.info("RENDER TEMPLATE: %s -> %s", template, public)
  42. self.render_template(template, public)
  43. def render_template(self, template, public):
  44. """
  45. Render a template to the public path.
  46. Parameters:
  47. template (str): path in ./templates
  48. public (str): path in ./public
  49. """
  50. tpl = self.env.get_template(template.replace("./templates/", ""))
  51. html = tpl.render(self.vars())
  52. with codecs.open(public, "w", "utf-8") as fh:
  53. fh.write(html)
  54. def crawl(self, path):
  55. """
  56. Crawl a directory recursively and return a list of file paths.
  57. Parameters:
  58. path (str)
  59. Returns:
  60. filepaths (list[str])
  61. """
  62. for root, dirs, files in os.walk(path):
  63. for file in files:
  64. yield os.path.join(root, file)
  65. def should_copy_static(self, static_file, public_file):
  66. """
  67. Determine if a static file should be copied over to the public folder.
  68. This means the public file either does not exist yet, or is older
  69. than the static file and so the new static file should be copied over it.
  70. Parameters:
  71. static_file (str)
  72. public_file (str)
  73. Returns:
  74. bool: True if public_file doesn't exist or is older than static_file.
  75. """
  76. if not os.path.exists(public_file):
  77. return True
  78. return os.stat(static_file).st_mtime > os.stat(public_file).st_mtime
  79. def template_to_public(self, template_filename):
  80. """
  81. Convert a template filename into a public name, preferring clean URL
  82. paths without file extension suffixes.
  83. Examples:
  84. * /index.html -> /index.html
  85. * /about.html -> /about/index.html
  86. * /photos/index.html -> /photos/index.html
  87. * /photos/2018.html -> /photos/2018/index.html
  88. The `.html` and `.md` file types will suppress suffixes in this way.
  89. Parameters:
  90. template_filename: like ``./templates/about.html``
  91. Returns:
  92. public_filename: like ``./public/about/index.html``
  93. """
  94. public = template_filename.replace("./templates/", "./public/")
  95. path, filename = public.rsplit("/", 1)
  96. basename, ext = filename.rsplit(".", 1)
  97. if ext not in ["html", "md"]:
  98. # Not a web page so just keep the path literal.
  99. return public
  100. print("template_to_public:", template_filename, public)
  101. # See if it's already an index page or if we need to create one.
  102. if filename in ["index.html", "index.md"]:
  103. return public # already a good name
  104. else:
  105. print("HERE:", os.path.join(path, basename, "index."+ext))
  106. log.info("basename=%r path=%r filename=%s", basename, path, filename)
  107. return os.path.join(path, basename, "index."+ext)
  108. def mkdir(self, path):
  109. """Create a directory and log it."""
  110. if not os.path.isdir(path):
  111. log.info("mkdir: %s", path)
  112. os.makedirs(path, mode=0o755)
  113. if __name__ == "__main__":
  114. App()