sitegen.py 6.7 KB

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