import argparse from functools import lru_cache from jinja2 import Environment, FileSystemLoader, contextfilter from pathlib import Path from os import walk, makedirs, listdir, symlink, readlink from os.path import join, exists, splitext, split, islink, isdir from shutil import rmtree, copy2, copystat, Error from time import sleep from traceback import print_exc #TODO: load from config file (and watch it too) LANGUAGES = 'languages' ROOT = 'root' STATIC = 'static' DEFAULT_LANG = 'en' OTHER_LANGS = set(['es']) WATCH_INTERVAL = 1 # in secs # Utils def copytree(src, dst, symlinks=False): names = listdir(src) makedirs(dst, exist_ok=True) # `exist_ok=True`, very important for hot reloading errors = [] for name in names: srcname = join(src, name) dstname = join(dst, name) try: if symlinks and islink(srcname): linkto = readlink(srcname) symlink(linkto, dstname) elif isdir(srcname): copytree(srcname, dstname, symlinks) else: copy2(srcname, dstname) # XXX What about devices, sockets etc.? except OSError as why: errors.append((srcname, dstname, str(why))) # catch the Error from the recursive copytree so that we can # continue with other files except Error as err: errors.extend(err.args[0]) try: copystat(src, dst) except OSError as why: # can't copy file access times on Windows if why.winerror is None: errors.extend((src, dst, str(why))) if errors: raise Error(errors) def mtimes(target_dir): ''''get modification time of files in `target_dir`''' return { f: f.stat().st_mtime for f in Path(target_dir).rglob('*') } def save_dic(lang, dic): with open(join(LANGUAGES, lang + '.txt'), 'w') as f: for key, value in dic.items(): f.write(f"{key}:{value}\n") @lru_cache(None) def get_lang_dic(lang): makedirs(LANGUAGES, exist_ok=True) lang_file = join(LANGUAGES, lang + '.txt') if not exists(lang_file): return {} else: dic = {} with open(lang_file) as f: for line in f.readlines(): key, value = line.strip('\n').split(':') dic[key] = value return dic # Filters def add_lang_prefix(lang, path): if lang == DEFAULT_LANG: return path if lang not in OTHER_LANGS: print(f"Not registered language: `{lang}`") return f"/{lang}{path}" @contextfilter def lang(ctx, value): lang = ctx.environment.globals['lang'] if lang == DEFAULT_LANG: return value else: dic = get_lang_dic(lang) if value in dic and dic[value]: return dic[value] else: dic[value] = "" save_dic(lang, dic) print(f"Not translated phrase: `{value}`") return value @contextfilter def lang_url(ctx, lang): name, _ = splitext(ctx.name) if name.startswith('./'): name = name[2:] if name == 'index': name = '' return add_lang_prefix(lang, f"/{name}") @contextfilter def cur_lang(ctx, path): lang = ctx.environment.globals['lang'] return add_lang_prefix(lang, path) def static(value): return f"/static/{value}" # Compiler def compile(env, path, target): lang = env.globals['lang'] for base, _, docs in walk(path): cur_base = Path(base) cur_base = Path(*cur_base.parts[1:]) for doc in docs: if doc.startswith('_'): # Ignore this files continue src = join(base, doc) if args.with_index and doc != 'index.html': name, _ = splitext(doc) dst = join(cur_base, name, 'index.html') else: dst = join(cur_base, doc) if lang != DEFAULT_LANG: dst = join(lang, dst) dst = join(target, dst) dst_folder, _ = split(dst) makedirs(dst_folder, exist_ok=True) template = env.get_template(join(cur_base, doc)) output = template.render() with open(dst, 'w') as f: f.write(output) def init_gen(args): # Load layout file_loader = FileSystemLoader(args.source) env = args.env = Environment(loader=file_loader) # Init gobals env.globals['lang'] = DEFAULT_LANG # Add filters env.filters['lang'] = lang env.filters['lang_url'] = lang_url env.filters['cur_lang'] = cur_lang env.filters['static'] = static # Clean target if exists(args.target): rmtree(args.target) makedirs(args.target, exist_ok=True) def gen_layout(args): env = args.env path = Path(args.source) # Reset gobals env.globals['lang'] = DEFAULT_LANG # compile compile(env, path, args.target) for other_lang in OTHER_LANGS: env.globals['lang'] = other_lang compile(env, path, args.target) def gen_root(args): copytree(ROOT, args.target) def gen_static(args): copytree(STATIC, join(args.target, 'static')) def save_generate(generator, args, msg): print(f'* {msg}') try: generator(args) print(' done!') except: print_exc() return False return True # Runtime def run(args): print('initial compilation') init_gen(args) save_generate(gen_layout, args, 'generating layout') save_generate(gen_root, args, 'copying root files') save_generate(gen_static, args, 'copying static dir') if not args.watch: return print(f'watching {args.source}/*:{LANGUAGES}/*:{STATIC}/*:{ROOT}/*') while True: # take modification times source_mt = mtimes(args.source) source_mt.update(mtimes(LANGUAGES)) static_mt = mtimes(STATIC) root_mt = mtimes(ROOT) sleep(WATCH_INTERVAL) # compare modification times and recompile if different new_source_mt = mtimes(args.source) new_source_mt.update(mtimes(LANGUAGES)) if source_mt != new_source_mt: source_mt = new_source_mt save_generate(gen_layout, args, 'recompiling layout') new_root_mt = mtimes(ROOT) if root_mt != new_root_mt: root_mt = new_root_mt save_generate(gen_root, args, 'copying root files') new_static_mt = mtimes(STATIC) if static_mt != new_static_mt: static_mt = new_static_mt save_generate(gen_static, args, 'copying static files') if __name__ == '__main__': parser = argparse.ArgumentParser("Site generator") parser.add_argument('--watch', action='store_true', default=False) parser.add_argument('--with-index', action='store_true', default=False) parser.add_argument('--source', default='layout') parser.add_argument('--target', default='build') args = parser.parse_args() run(args)