sitegen.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. import argparse
  2. from functools import lru_cache
  3. from jinja2 import Environment, FileSystemLoader, contextfilter
  4. from pathlib import Path
  5. from os import walk, makedirs, listdir, symlink, readlink
  6. from os.path import join, exists, splitext, split, islink, isdir
  7. from shutil import rmtree, copy2, copystat, Error
  8. from time import sleep
  9. from traceback import print_exc
  10. #TODO: load from config file (and watch it too)
  11. LANGUAGES = 'languages'
  12. ROOT = 'root'
  13. STATIC = 'static'
  14. DEFAULT_LANG = 'en'
  15. OTHER_LANGS = set(['es'])
  16. WATCH_INTERVAL = 1 # in secs
  17. # Utils
  18. def copytree(src, dst, symlinks=False):
  19. names = listdir(src)
  20. makedirs(dst, exist_ok=True)
  21. # `exist_ok=True`, very important for hot reloading
  22. errors = []
  23. for name in names:
  24. srcname = join(src, name)
  25. dstname = join(dst, name)
  26. try:
  27. if symlinks and islink(srcname):
  28. linkto = readlink(srcname)
  29. symlink(linkto, dstname)
  30. elif isdir(srcname):
  31. copytree(srcname, dstname, symlinks)
  32. else:
  33. copy2(srcname, dstname)
  34. # XXX What about devices, sockets etc.?
  35. except OSError as why:
  36. errors.append((srcname, dstname, str(why)))
  37. # catch the Error from the recursive copytree so that we can
  38. # continue with other files
  39. except Error as err:
  40. errors.extend(err.args[0])
  41. try:
  42. copystat(src, dst)
  43. except OSError as why:
  44. # can't copy file access times on Windows
  45. if why.winerror is None:
  46. errors.extend((src, dst, str(why)))
  47. if errors:
  48. raise Error(errors)
  49. def mtimes(target_dir):
  50. ''''get modification time of files in `target_dir`'''
  51. return { f: f.stat().st_mtime for f in Path(target_dir).rglob('*') }
  52. def save_dic(lang, dic):
  53. with open(join(LANGUAGES, lang + '.txt'), 'w') as f:
  54. for key, value in dic.items():
  55. f.write(f"{key}:{value}\n")
  56. @lru_cache(None)
  57. def get_lang_dic(lang):
  58. makedirs(LANGUAGES, exist_ok=True)
  59. lang_file = join(LANGUAGES, lang + '.txt')
  60. if not exists(lang_file):
  61. return {}
  62. else:
  63. dic = {}
  64. with open(lang_file) as f:
  65. for line in f.readlines():
  66. key, value = line.strip('\n').split(':')
  67. dic[key] = value
  68. return dic
  69. # Filters
  70. def add_lang_prefix(lang, path):
  71. if lang == DEFAULT_LANG:
  72. return path
  73. if lang not in OTHER_LANGS:
  74. print(f"Not registered language: `{lang}`")
  75. return f"/{lang}{path}"
  76. @contextfilter
  77. def lang(ctx, value):
  78. lang = ctx.environment.globals['lang']
  79. if lang == DEFAULT_LANG:
  80. return value
  81. else:
  82. dic = get_lang_dic(lang)
  83. if value in dic and dic[value]:
  84. return dic[value]
  85. else:
  86. dic[value] = ""
  87. save_dic(lang, dic)
  88. print(f"Not translated phrase: `{value}`")
  89. return value
  90. @contextfilter
  91. def lang_url(ctx, lang):
  92. name, _ = splitext(ctx.name)
  93. if name.startswith('./'):
  94. name = name[2:]
  95. if name == 'index':
  96. name = ''
  97. return add_lang_prefix(lang, f"/{name}")
  98. @contextfilter
  99. def cur_lang(ctx, path):
  100. lang = ctx.environment.globals['lang']
  101. return add_lang_prefix(lang, path)
  102. def static(value):
  103. return f"/static/{value}"
  104. # Compiler
  105. def compile(env, path, target):
  106. lang = env.globals['lang']
  107. for base, _, docs in walk(path):
  108. cur_base = Path(base)
  109. cur_base = Path(*cur_base.parts[1:])
  110. for doc in docs:
  111. if doc.startswith('_'):
  112. # Ignore this files
  113. continue
  114. src = join(base, doc)
  115. if args.with_index and doc != 'index.html':
  116. name, _ = splitext(doc)
  117. dst = join(cur_base, name, 'index.html')
  118. else:
  119. dst = join(cur_base, doc)
  120. if lang != DEFAULT_LANG:
  121. dst = join(lang, dst)
  122. dst = join(target, dst)
  123. dst_folder, _ = split(dst)
  124. makedirs(dst_folder, exist_ok=True)
  125. template = env.get_template(join(cur_base, doc))
  126. output = template.render()
  127. with open(dst, 'w') as f:
  128. f.write(output)
  129. def init_gen(args):
  130. # Load layout
  131. file_loader = FileSystemLoader(args.source)
  132. env = args.env = Environment(loader=file_loader)
  133. # Init gobals
  134. env.globals['lang'] = DEFAULT_LANG
  135. # Add filters
  136. env.filters['lang'] = lang
  137. env.filters['lang_url'] = lang_url
  138. env.filters['cur_lang'] = cur_lang
  139. env.filters['static'] = static
  140. # Clean target
  141. if exists(args.target):
  142. rmtree(args.target)
  143. makedirs(args.target, exist_ok=True)
  144. def gen_layout(args):
  145. env = args.env
  146. path = Path(args.source)
  147. # Reset gobals
  148. env.globals['lang'] = DEFAULT_LANG
  149. # compile
  150. compile(env, path, args.target)
  151. for other_lang in OTHER_LANGS:
  152. env.globals['lang'] = other_lang
  153. compile(env, path, args.target)
  154. def gen_root(args):
  155. copytree(ROOT, args.target)
  156. def gen_static(args):
  157. copytree(STATIC, join(args.target, 'static'))
  158. def save_generate(generator, args, msg):
  159. print(f'* {msg}')
  160. try:
  161. generator(args)
  162. print(' done!')
  163. except:
  164. print_exc()
  165. return False
  166. return True
  167. # Runtime
  168. def run(args):
  169. print('initial compilation')
  170. init_gen(args)
  171. save_generate(gen_layout, args, 'generating layout')
  172. save_generate(gen_root, args, 'copying root files')
  173. save_generate(gen_static, args, 'copying static dir')
  174. if not args.watch:
  175. return
  176. print(f'watching {args.source}/*:{LANGUAGES}/*:{STATIC}/*:{ROOT}/*')
  177. while True:
  178. # take modification times
  179. source_mt = mtimes(args.source)
  180. source_mt.update(mtimes(LANGUAGES))
  181. static_mt = mtimes(STATIC)
  182. root_mt = mtimes(ROOT)
  183. sleep(WATCH_INTERVAL)
  184. # compare modification times and recompile if different
  185. new_source_mt = mtimes(args.source)
  186. new_source_mt.update(mtimes(LANGUAGES))
  187. if source_mt != new_source_mt:
  188. source_mt = new_source_mt
  189. save_generate(gen_layout, args, 'recompiling layout')
  190. new_root_mt = mtimes(ROOT)
  191. if root_mt != new_root_mt:
  192. root_mt = new_root_mt
  193. save_generate(gen_root, args, 'copying root files')
  194. new_static_mt = mtimes(STATIC)
  195. if static_mt != new_static_mt:
  196. static_mt = new_static_mt
  197. save_generate(gen_static, args, 'copying static files')
  198. if __name__ == '__main__':
  199. parser = argparse.ArgumentParser("Site generator")
  200. parser.add_argument('--watch', action='store_true', default=False)
  201. parser.add_argument('--with-index', action='store_true', default=False)
  202. parser.add_argument('--source', default='layout')
  203. parser.add_argument('--target', default='build')
  204. args = parser.parse_args()
  205. run(args)